mirror of
https://github.com/cline/cline.git
synced 2026-09-11 05:47:07 +08:00
Compare commits
224
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90064ab6d4 | ||
|
|
5249f417a1 | ||
|
|
7c911e96eb | ||
|
|
9385b17f00 | ||
|
|
9e5baa5b29 | ||
|
|
3e3216afaa | ||
|
|
fa31e0cff6 | ||
|
|
3bffa3e554 | ||
|
|
60029a269c | ||
|
|
89aba0742c | ||
|
|
24d8a1b222 | ||
|
|
013efb6e2f | ||
|
|
20abf70948 | ||
|
|
f7c0dc51e8 | ||
|
|
2333423731 | ||
|
|
c743e22f30 | ||
|
|
7bb9a9ca48 | ||
|
|
091933e608 | ||
|
|
aca18219bd | ||
|
|
0c1dc067ce | ||
|
|
1ed576f693 | ||
|
|
3087a3c058 | ||
|
|
601c3a99af | ||
|
|
da3d218410 | ||
|
|
5cdacca80c | ||
|
|
0b5024dc42 | ||
|
|
23a2c24366 | ||
|
|
58cfed5613 | ||
|
|
20bdc3cead | ||
|
|
525d1b6e21 | ||
|
|
d5169f32f7 | ||
|
|
e23b953a02 | ||
|
|
cd8690b939 | ||
|
|
fd961c2cbc | ||
|
|
1691177bee | ||
|
|
488adb42a2 | ||
|
|
b59aa52c9e | ||
|
|
06f40a75c6 | ||
|
|
49eb4f95da | ||
|
|
ec0fde8602 | ||
|
|
bee130950e | ||
|
|
1fb423bc8e | ||
|
|
7905376e20 | ||
|
|
01740fbda9 | ||
|
|
d4622a75df | ||
|
|
dd145671af | ||
|
|
1bb2962b26 | ||
|
|
7afd207b4d | ||
|
|
28c7ffd216 | ||
|
|
dbc0e8aa1d | ||
|
|
714236a233 | ||
|
|
6c152bc307 | ||
|
|
a9f7e014f9 | ||
|
|
f9e758a29e | ||
|
|
74591cb254 | ||
|
|
9c273551e2 | ||
|
|
9d40ee4529 | ||
|
|
2140db1aae | ||
|
|
6714587e6e | ||
|
|
30dcfe05c3 | ||
|
|
ce1d3bc5b9 | ||
|
|
f5fb30e98a | ||
|
|
67ec7bb8b2 | ||
|
|
2c42b46e71 | ||
|
|
af19e0d560 | ||
|
|
27c7720f71 | ||
|
|
9b27ac8cb9 | ||
|
|
be9fbb01bc | ||
|
|
82331aff2d | ||
|
|
7d9c69687f | ||
|
|
0916c78d52 | ||
|
|
b31c3c5c1d | ||
|
|
484807c472 | ||
|
|
23490f7e6f | ||
|
|
8ea0543b89 | ||
|
|
c7c9813056 | ||
|
|
ff2bdde179 | ||
|
|
a27e0bddbf | ||
|
|
32199574a9 | ||
|
|
fccb633d08 | ||
|
|
f5679b5c91 | ||
|
|
ee88ddd678 | ||
|
|
ee50091c9a | ||
|
|
f6cc991b48 | ||
|
|
02b6c1fc91 | ||
|
|
5df13d8f32 | ||
|
|
918e32edee | ||
|
|
06d8e7ae9f | ||
|
|
2b35590de8 | ||
|
|
2788d85556 | ||
|
|
4ffb28377a | ||
|
|
fa570fa500 | ||
|
|
23f64d24c6 | ||
|
|
f4c79e2d21 | ||
|
|
10449e458e | ||
|
|
2ac5a0ea1f | ||
|
|
cb34c643d7 | ||
|
|
d9f4946665 | ||
|
|
cb2355d881 | ||
|
|
04ce2878aa | ||
|
|
c52d47decd | ||
|
|
26ca343dae | ||
|
|
81781a6143 | ||
|
|
e2c92051f4 | ||
|
|
f09a5d79ef | ||
|
|
8822846800 | ||
|
|
c2a7836d0c | ||
|
|
e84ff527c4 | ||
|
|
349d7cf24b | ||
|
|
d5af7a2bad | ||
|
|
77a4bfa3f8 | ||
|
|
d0d9ae5e9a | ||
|
|
0d188ade2f | ||
|
|
d043ba9ec9 | ||
|
|
dc35c62d5d | ||
|
|
2dc54861ba | ||
|
|
5cff5f3f24 | ||
|
|
39bb06bb93 | ||
|
|
599e236546 | ||
|
|
06f015c9d0 | ||
|
|
49bdafe10e | ||
|
|
955b06d4fa | ||
|
|
34ac2176e6 | ||
|
|
c5e87a800b | ||
|
|
92c9cc5ddc | ||
|
|
dd0b7697f2 | ||
|
|
9d68aa8b8e | ||
|
|
c59fe3e62c | ||
|
|
7602f7b7c6 | ||
|
|
a1debe1328 | ||
|
|
990a00d70f | ||
|
|
a125a61cbd | ||
|
|
e3e6e1ef77 | ||
|
|
09e897c8f2 | ||
|
|
6caeeb5da6 | ||
|
|
40550af42c | ||
|
|
e85e4d6f0c | ||
|
|
d26f76c6e4 | ||
|
|
5c6f725079 | ||
|
|
4a456e2145 | ||
|
|
c029e0ffe4 | ||
|
|
cff2217283 | ||
|
|
904617b573 | ||
|
|
20fae7e14b | ||
|
|
699ed190b7 | ||
|
|
28ae6d6ca1 | ||
|
|
beca76fb32 | ||
|
|
8de9935f9a | ||
|
|
6bc700e3db | ||
|
|
2f17a1a341 | ||
|
|
639121b644 | ||
|
|
1187852bed | ||
|
|
7acc036d91 | ||
|
|
1fea5aaa63 | ||
|
|
8de7294de8 | ||
|
|
2e4ded7f14 | ||
|
|
ac94eb4cf4 | ||
|
|
73635cf68f | ||
|
|
9eb0da6c2b | ||
|
|
59c10df046 | ||
|
|
db4d9a267a | ||
|
|
734dc6e57f | ||
|
|
27befb16f5 | ||
|
|
1c32b08644 | ||
|
|
5d81bf0fe2 | ||
|
|
f44771acd5 | ||
|
|
786728e0d4 | ||
|
|
9714cf8861 | ||
|
|
c5db44964e | ||
|
|
ed38739d65 | ||
|
|
50bc90835c | ||
|
|
4d41766b9f | ||
|
|
e958e29439 | ||
|
|
b337bb706e | ||
|
|
314e06731a | ||
|
|
3a6f8b98dd | ||
|
|
65f9322bd9 | ||
|
|
33983e80f8 | ||
|
|
8648154a74 | ||
|
|
5d91ac037e | ||
|
|
6179dfe0a4 | ||
|
|
cd9e28e6d0 | ||
|
|
8d56a697eb | ||
|
|
fd506b14b5 | ||
|
|
55d268773d | ||
|
|
bf978c2c42 | ||
|
|
7e06dbbd33 | ||
|
|
8d4822d781 | ||
|
|
dbcaae65d1 | ||
|
|
66db36ff5b | ||
|
|
a53d17a857 | ||
|
|
de56305315 | ||
|
|
5cd22e3f86 | ||
|
|
bd9150837d | ||
|
|
fbb494b88a | ||
|
|
16b4c48457 | ||
|
|
058c86872b | ||
|
|
d3e8f562bc | ||
|
|
1594d44190 | ||
|
|
c8419ffa99 | ||
|
|
cf6c80504f | ||
|
|
230f615927 | ||
|
|
08e00dc3bf | ||
|
|
c01bf73d3f | ||
|
|
df28359114 | ||
|
|
d3631f0485 | ||
|
|
eb18c556f4 | ||
|
|
b18284ea78 | ||
|
|
e4e99ec711 | ||
|
|
b7b4d0bfe8 | ||
|
|
c12b00e7a9 | ||
|
|
73bb8c2f64 | ||
|
|
0e1d513462 | ||
|
|
d6a1b338bc | ||
|
|
0091fb6ffb | ||
|
|
de3e3fe34a | ||
|
|
e136d75621 | ||
|
|
91127ffdb9 | ||
|
|
11f76f0218 | ||
|
|
74f1b9c75d | ||
|
|
942c78119e | ||
|
|
3130c3c96e | ||
|
|
f0225bc20d | ||
|
|
1bf158245c |
@@ -0,0 +1,8 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$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": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add endpoint configuration file support for on-premise deployments
|
||||
|
||||
Enterprise customers can now configure custom API endpoints by creating a `~/.cline/endpoints.json` file with custom URLs for `appBaseUrl`, `apiBaseUrl`, and `mcpBaseUrl`. When this file is present, Cline runs in on-premise mode with the custom endpoints.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: prevent infinite retry loops when replace_in_file fails repeatedly
|
||||
|
||||
Add safeguards to prevent the LLM from getting stuck in infinite retry loops when `replace_in_file` operations fail repeatedly. The system now detects repeated failures and provides better guidance to break out of retry cycles.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: skip diff error UI handling during streaming to prevent flickering
|
||||
|
||||
Suppress diff view error notifications while content is actively streaming to prevent visual flickering and improve user experience. Error handling is deferred until streaming completes.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix(extract-text): strip notebook outputs to reduce context size
|
||||
|
||||
Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing the amount of context sent to the LLM while preserving the essential code and markdown content.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: throttle diff view updates during streaming
|
||||
|
||||
Add throttling to diff view updates during content streaming to reduce UI flickering and improve performance. Updates are now batched at reasonable intervals instead of firing on every token received.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Disable PostHog telemetry, error tracking, and feature flags in self-hosted mode
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Remove deprecated zai-glm-4.6 model from Cerebras provider
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Make Sonnet 4.5 the default Amazon Bedrock model id
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Disable PostHog and build-time OpenTelemetry telemetry in self-hosted/on-premise mode. Enterprise customers running self-hosted deployments will no longer send any telemetry to Cline's collectors. Runtime environment OTEL and remote config OTEL remain available for enterprises to configure their own telemetry collection.
|
||||
@@ -0,0 +1,26 @@
|
||||
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_
|
||||
+6
-21
@@ -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, PR template usage, 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, and PR creation using the gh CLI tool.
|
||||
---
|
||||
|
||||
# Create Pull Request
|
||||
@@ -147,29 +147,14 @@ When filling out the template:
|
||||
|
||||
### Create PR with gh CLI
|
||||
|
||||
**Use a temporary file for the PR body** to avoid shell escaping issues, newline problems, and other command-line flakiness:
|
||||
|
||||
1. Write the PR body to a temporary file:
|
||||
```
|
||||
/tmp/pr-body.md
|
||||
```
|
||||
|
||||
2. Create the PR using the file:
|
||||
```bash
|
||||
gh pr create --title "PR_TITLE" --body-file /tmp/pr-body.md --base main
|
||||
```
|
||||
|
||||
3. Clean up the temporary file:
|
||||
```bash
|
||||
rm /tmp/pr-body.md
|
||||
```
|
||||
|
||||
For draft PRs:
|
||||
```bash
|
||||
gh pr create --title "PR_TITLE" --body-file /tmp/pr-body.md --base main --draft
|
||||
gh pr create --title "PR_TITLE" --body "PR_BODY" --base main
|
||||
```
|
||||
|
||||
**Why use a file?** Passing complex markdown with newlines, special characters, and checkboxes directly via `--body` is error-prone. The `--body-file` flag handles all content reliably.
|
||||
Alternatively, create as draft if the user wants review before marking ready:
|
||||
```bash
|
||||
gh pr create --title "PR_TITLE" --body "PR_BODY" --base main --draft
|
||||
```
|
||||
|
||||
## Post-Creation
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
# CLI Development
|
||||
|
||||
The CLI lives in `cli/` and uses React Ink for terminal UI.
|
||||
The CLI lives in `cli-ts/` and uses React Ink for terminal UI.
|
||||
|
||||
- If needed, look at `cli/src/constants/colors.ts` for re-used terminal colors, e.g. `COLORS.primaryBlue` highlight color (selections, spinners, success states).
|
||||
- If needed, look at `cli-ts/src/constants/colors.ts` for re-used terminal colors, e.g. `COLORS.primaryBlue` highlight color (selections, spinners, success states).
|
||||
- Never use `dimColor` with gray (e.g. `<Text color="gray" dimColor>`) - it's too hard to read. Use `color="gray"` for secondary text and normal foreground (no color) for primary text.
|
||||
- When thinking about how to handle state or messages from core, look at webview for how it communicates with the vs code extension.
|
||||
- When updating the webview, consider and suggest to the user to update the CLI TUI since we want to provide a similar experience to our terminal users as we do our vs code extension users.
|
||||
@@ -11,7 +11,7 @@ The CLI lives in `cli/` and uses React Ink for terminal UI.
|
||||
|
||||
When adding a new API provider to the extension, you must also update the CLI:
|
||||
|
||||
1. **Update `cli/src/components/ModelPicker.tsx`**: Add the provider to the `providerModels` map so `getDefaultModelId()` returns the correct default model. Import the models and default ID from `@shared/api`:
|
||||
1. **Update `cli-ts/src/components/ModelPicker.tsx`**: Add the provider to the `providerModels` map so `getDefaultModelId()` returns the correct default model. Import the models and default ID from `@shared/api`:
|
||||
```typescript
|
||||
import { newProviderDefaultModelId, newProviderModels } from "@/shared/api"
|
||||
|
||||
@@ -21,7 +21,7 @@ When adding a new API provider to the extension, you must also update the CLI:
|
||||
}
|
||||
```
|
||||
|
||||
2. **Use `applyProviderConfig()` for auth flows**: When implementing OAuth or other auth flows for the provider, use the shared utility at `cli/src/utils/provider-config.ts`:
|
||||
2. **Use `applyProviderConfig()` for auth flows**: When implementing OAuth or other auth flows for the provider, use the shared utility at `cli-ts/src/utils/provider-config.ts`:
|
||||
```typescript
|
||||
import { applyProviderConfig } from "../utils/provider-config"
|
||||
|
||||
|
||||
+1
-12
@@ -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, contributors should not create changelog-entry files. Maintainers handle release versioning and changelog curation during the release process.
|
||||
- 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 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
|
||||
|
||||
@@ -147,17 +147,6 @@ Required steps:
|
||||
|
||||
Common mistake: Adding only the return value without the `context.globalState.get()` call. This compiles but the value is always `undefined` on load.
|
||||
|
||||
Settings plumbing gotcha: if a key is user-toggleable from settings, wire both controller update paths:
|
||||
- `src/core/controller/state/updateSettings.ts` for webview `updateSetting(...)`
|
||||
- `src/core/controller/state/updateSettingsCli.ts` for CLI/ACP settings updates
|
||||
Missing one path causes a toggle to appear to change in one surface while the backend state stays unchanged.
|
||||
|
||||
Webview toggle gotcha: settings changes must also round-trip back in state payloads.
|
||||
- Add the field to `UpdateSettingsRequest` in `proto/cline/state.proto` (for webview update requests), then run `npm run protos`
|
||||
- Include the key in `Controller.getStateToPostToWebview()` (`src/core/controller/index.ts`)
|
||||
- Ensure `ExtensionState` and webview defaults include the key (`src/shared/ExtensionMessage.ts`, `webview-ui/src/context/ExtensionStateContext.tsx`)
|
||||
If this round-trip wiring is missing, the backend value can update but the toggle in webview appears stuck or reverts.
|
||||
|
||||
## StateManager Cache vs Direct globalState Access
|
||||
StateManager uses an in-memory cache populated during `StateManager.initialize(context)` in `common.ts`. For most state, use `controller.stateManager.setGlobalState()`/`getGlobalStateKey()`.
|
||||
|
||||
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/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
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/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
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/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
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/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
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/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
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/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
|
||||
@@ -1,64 +0,0 @@
|
||||
# Storage Architecture
|
||||
|
||||
Global settings, secrets and workspace state are stored in **file-backed JSON stores** under `~/.cline/data/`. This is the shared storage layer used by VSCode, CLI, and JetBrains.
|
||||
|
||||
## Key Abstractions
|
||||
|
||||
### `StorageContext` (src/shared/storage/storage-context.ts)
|
||||
The entry point. Created via `createStorageContext()` and passed to `StateManager.initialize()`. Contains three `ClineFileStorage` instances:
|
||||
- `globalState` → `~/.cline/data/globalState.json`
|
||||
- `secrets` → `~/.cline/data/secrets.json` (mode 0o600)
|
||||
- `workspaceState` → `~/.cline/data/workspaces/<hash>/workspaceState.json`
|
||||
|
||||
### `ClineFileStorage` (src/shared/storage/ClineFileStorage.ts)
|
||||
Synchronous JSON key-value store backed by a single file. Supports `get()`, `set()`, `setBatch()`, `delete()`. Writes are atomic (write-then-rename).
|
||||
|
||||
### `StateManager` (src/core/storage/StateManager.ts)
|
||||
In-memory cache on top of `StorageContext`. All runtime reads hit the cache; writes update cache immediately and debounce-flush to disk.
|
||||
|
||||
## ⚠️ Do NOT Use VSCode's ExtensionContext for Storage
|
||||
|
||||
**Do not** read from or write to `context.globalState`, `context.workspaceState`, or `context.secrets` for persistent data. These are VSCode-specific and not available on CLI or JetBrains.
|
||||
|
||||
Instead, use:
|
||||
```typescript
|
||||
// Reading state
|
||||
StateManager.get().getGlobalStateKey("myKey")
|
||||
StateManager.get().getSecretKey("mySecretKey")
|
||||
StateManager.get().getWorkspaceStateKey("myWsKey")
|
||||
|
||||
// Writing state
|
||||
StateManager.get().setGlobalState("myKey", value)
|
||||
StateManager.get().setSecret("mySecretKey", value)
|
||||
StateManager.get().setWorkspaceState("myWsKey", value)
|
||||
```
|
||||
|
||||
Remember that your data may be read by a different client than the one that wrote it. For example, a value written by Cline in JetBrains may be read by Cline CLI.
|
||||
|
||||
## VSCode Migration (src/hosts/vscode/vscode-to-file-migration.ts)
|
||||
|
||||
On VSCode startup, a migration copies data from VSCode's `ExtensionContext` storage into the file-backed stores. This runs in `src/common.ts` before `StateManager.initialize()`.
|
||||
|
||||
- **Sentinel**: `__vscodeMigrationVersion` key in global state and workspace state — prevents re-migration.
|
||||
- **Merge strategy**: File store wins. Existing values are never overwritten.
|
||||
- **Safe downgrade**: VSCode storage is NOT cleared, so older extension versions still work.
|
||||
|
||||
## Adding New Storage Keys
|
||||
|
||||
1. Add to `src/shared/storage/state-keys.ts` (see existing patterns)
|
||||
2. Read/write via `StateManager` (NOT via `context.globalState`)
|
||||
3. If adding a secret, add to `SecretKeys` array in `state-keys.ts`
|
||||
|
||||
## File Layout
|
||||
|
||||
```
|
||||
~/.cline/
|
||||
data/
|
||||
globalState.json # Global settings & state
|
||||
secrets.json # API keys (mode 0o600)
|
||||
tasks/
|
||||
taskHistory.json # Task history (separate file)
|
||||
workspaces/
|
||||
<hash>/
|
||||
workspaceState.json # Per-workspace toggles
|
||||
```
|
||||
@@ -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 (release automation, CI status, etc.).
|
||||
4. Present a summary of all comments with your recommendation for each (apply, skip, or respond). Ignore bot noise (changeset-bot, CI status, etc.).
|
||||
|
||||
5. **Wait for my approval** before proceeding.
|
||||
|
||||
|
||||
@@ -0,0 +1,549 @@
|
||||
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,9 +89,16 @@ On the main branch, create a commit that updates:
|
||||
|
||||
2. **package.json** - Update the version field to the new version
|
||||
|
||||
3. No changelog-entry file cleanup is needed. Contributors do not create changelog-entry files in this repo.
|
||||
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.
|
||||
|
||||
**Skip running `npm run install:all`** - release automation handles lockfile consistency as needed.
|
||||
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.
|
||||
|
||||
Commit with message format: `v{VERSION} Release Notes (hotfix)`
|
||||
|
||||
@@ -100,7 +107,7 @@ In the commit body, mention:
|
||||
- List the cherry-picked commits that will be included
|
||||
|
||||
```bash
|
||||
git add CHANGELOG.md package.json
|
||||
git add CHANGELOG.md package.json .changeset/
|
||||
git commit -m "v3.40.1 Release Notes (hotfix)
|
||||
|
||||
Hotfix release including:
|
||||
|
||||
@@ -347,6 +347,8 @@ 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,64 +1,232 @@
|
||||
# Release
|
||||
|
||||
Prepare and publish a release directly from `main`.
|
||||
Prepare and publish a release from the open changeset PR.
|
||||
|
||||
## Overview
|
||||
|
||||
This workflow helps you:
|
||||
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
|
||||
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
|
||||
|
||||
## Process
|
||||
## Step 1: Find the Changeset PR
|
||||
|
||||
### 1) Sync and determine version
|
||||
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:
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
cat package.json | grep '"version"'
|
||||
```
|
||||
|
||||
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
|
||||
**IMPORTANT: Verify the latest commit is the release commit before tagging:**
|
||||
|
||||
```bash
|
||||
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>
|
||||
git log -1 --oneline
|
||||
```
|
||||
|
||||
### 4) Trigger publish workflow
|
||||
Confirm the commit message matches `v{VERSION} Release Notes` (e.g., `v3.44.1 Release Notes`). Do NOT blindly tag HEAD without verification.
|
||||
|
||||
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:
|
||||
Once verified, tag and push:
|
||||
|
||||
```bash
|
||||
gh release view v<version> --json body --jq '.body'
|
||||
gh release edit v<version> --notes "<final curated release notes>"
|
||||
VERSION=<version>
|
||||
git tag v${VERSION}
|
||||
git push origin v${VERSION}
|
||||
```
|
||||
|
||||
### 6) Final summary
|
||||
## Step 8: Trigger Publish Workflow
|
||||
|
||||
Provide:
|
||||
- Released version/tag
|
||||
- Link to release page
|
||||
- Summary of top end-user changes
|
||||
**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
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
|
||||
version = 1
|
||||
name = "cline"
|
||||
|
||||
[setup]
|
||||
script = '''
|
||||
if [ ! -d "node_modules" ]; then
|
||||
MAIN_WORKTREE="$(git worktree list | head -n1 | awk '{print $1}')"
|
||||
ln -s "$MAIN_WORKTREE/node_modules" node_modules
|
||||
ln -s "$MAIN_WORKTREE/webview-ui/node_modules" webview-ui/node_modules
|
||||
fi
|
||||
'''
|
||||
|
||||
[[actions]]
|
||||
name = "VS Code"
|
||||
icon = "run"
|
||||
command = "chmod +x ./scripts/run-extension-host.sh && ./scripts/run-extension-host.sh production"
|
||||
|
||||
[[actions]]
|
||||
name = "CLI"
|
||||
icon = "run"
|
||||
command = '''
|
||||
npm run cli:build
|
||||
npm run cli:run
|
||||
'''
|
||||
|
||||
[[actions]]
|
||||
name = "npm install"
|
||||
icon = "tool"
|
||||
command = '''
|
||||
rm node_modules
|
||||
rm webview-ui/node_modules
|
||||
npm run install:all
|
||||
'''
|
||||
|
||||
[[actions]]
|
||||
name = "pull main"
|
||||
icon = "tool"
|
||||
command = '''
|
||||
git fetch origin main
|
||||
|
||||
if ! git merge-base --is-ancestor main origin/main; then
|
||||
echo "Local main has commits not on origin/main. Aborting..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git update-ref refs/heads/main refs/remotes/origin/main
|
||||
echo "main updated to $(git rev-parse --short main)"
|
||||
'''
|
||||
@@ -16,6 +16,13 @@
|
||||
TELEMETRY_SERVICE_API_KEY=your-posthog-telemetry-api-key
|
||||
ERROR_SERVICE_API_KEY=your-posthog-error-tracking-api-key
|
||||
|
||||
# ============================================================================
|
||||
# TELEMETRY PROVIDER CONTROL
|
||||
# ============================================================================
|
||||
# Control which telemetry providers are active
|
||||
POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: true)
|
||||
# Set to false to disable Telemetry completely
|
||||
|
||||
# ============================================================================
|
||||
# OPENTELEMETRY (Optional - for advanced telemetry)
|
||||
# ============================================================================
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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
|
||||
|
||||
+3
-2
@@ -1,2 +1,3 @@
|
||||
/.github/ @saoudrizwan @arafatkatze @maxpaulus43 @candieduniverse
|
||||
/README.md @saoudrizwan @juanpflores
|
||||
/docs/
|
||||
/.github/ @saoudrizwan @garoth @sjf
|
||||
/README.md @saoudrizwan @nickbaumann98
|
||||
|
||||
@@ -26,12 +26,6 @@ body:
|
||||
placeholder: 'e.g., 1.2.3'
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: beta
|
||||
attributes:
|
||||
label: Beta version
|
||||
options:
|
||||
- label: I am using a beta version of Cline
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
# 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,6 +60,7 @@ 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
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
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!")
|
||||
@@ -0,0 +1,113 @@
|
||||
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,173 @@
|
||||
name: Claude Issue Triage
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
# Manual trigger for backfilling existing issues. Run from terminal:
|
||||
# gh workflow run claude-issue-triage.yml -f issue_number=1234
|
||||
# Or batch process:
|
||||
# gh issue list --state open --limit 10 --json number --jq '.[].number' | while read num; do
|
||||
# gh workflow run claude-issue-triage.yml -f issue_number=$num
|
||||
# sleep 60
|
||||
# done
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: 'Issue number to triage'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
claude-issue-triage:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
# SECURITY: These permissions are intentionally restrictive.
|
||||
# - contents: read -> Claude can read the codebase but CANNOT write/push any code
|
||||
# - issues: write -> Claude can comment and add labels (the only write access needed)
|
||||
# - pull-requests: read -> Claude can view PR context but CANNOT create PRs
|
||||
# This ensures that even if a malicious user attempts prompt injection via issue content,
|
||||
# Claude cannot modify repository code, create branches, or open PRs.
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Run Issue Response & Triage
|
||||
id: triage
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
allowed_non_write_users: "*"
|
||||
# Allow all tools - security is enforced by GitHub permissions above (contents: read, issues: write)
|
||||
claude_args: --model claude-opus-4-5-20251101 --allowedTools "Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch"
|
||||
prompt: |
|
||||
You're a GitHub issue first responder for the open source Cline repository.
|
||||
|
||||
**Issue:** #${{ github.event.issue.number || inputs.issue_number }}
|
||||
**Title:** ${{ github.event.issue.title || 'See issue details below' }}
|
||||
**Author:** @${{ github.event.issue.user.login || 'See issue details below' }}
|
||||
|
||||
## Your job
|
||||
|
||||
Investigate this issue thoroughly, then post a single helpful comment that helps the user and gives maintainers the context they need.
|
||||
|
||||
## Investigation
|
||||
|
||||
Start by reading the full issue:
|
||||
gh issue view ${{ github.event.issue.number || inputs.issue_number }}
|
||||
|
||||
### Search for duplicates and related issues
|
||||
|
||||
Search thoroughly for existing issues that match this one:
|
||||
gh issue list --search "<keywords from the issue>" --state all --limit 30
|
||||
gh issue list --search "<error messages>" --state all --limit 20
|
||||
gh issue list --search "<affected feature/component>" --state all --limit 20
|
||||
|
||||
For each relevant issue you find, read it including its comments:
|
||||
gh issue view <number> --comments
|
||||
|
||||
You're looking for:
|
||||
- **Duplicates**: Issues describing the same problem. Link to them and explain why you think they're duplicates. If closed, check how they were resolved - the solution might apply here.
|
||||
- **Related issues**: Similar problems or context that could help. Pull useful information from their comments (workarounds others found, debugging steps that helped, maintainer explanations). Link to them and explain the connection.
|
||||
|
||||
If there are closed issues with solutions, surface those solutions prominently - this might immediately solve the user's problem.
|
||||
|
||||
### Analyze recent changes (ALWAYS DO THIS)
|
||||
|
||||
Many issues are regressions from recent releases. **Always** check what changed recently:
|
||||
gh release list --limit 10
|
||||
gh pr list --state merged --limit 50 --json number,title,mergedAt,author,body
|
||||
|
||||
Look for PRs merged in the last few weeks that might correlate with the issue. If you find a likely connection:
|
||||
gh pr view <number>
|
||||
gh pr diff <number>
|
||||
git log --since="1 month ago" --oneline -- <relevant paths>
|
||||
git show <commit>
|
||||
|
||||
**Always include your findings in your comment:**
|
||||
- If you find a regression, call it out explicitly: which PR/commit likely caused it, who authored it, what changed, and suggest a fix direction if you can see one.
|
||||
- If you don't find anything related, still mention it: "I analyzed recent PRs and releases but didn't find any changes that seem related to this issue."
|
||||
|
||||
### Search the codebase
|
||||
|
||||
Find the relevant code:
|
||||
- Use grep/find to locate code related to the issue
|
||||
- Key areas: `src/api/` (providers/models), `src/core/prompts/` (tools/prompts), platform-specific code for VS Code vs JetBrains
|
||||
|
||||
### Find documentation
|
||||
|
||||
Cline docs are at **https://docs.cline.bot/** and built with Mintlify from the `docs/` directory.
|
||||
|
||||
The URL structure maps directly to the file structure:
|
||||
- `docs/getting-started/selecting-your-model.mdx` → https://docs.cline.bot/getting-started/selecting-your-model
|
||||
- `docs/troubleshooting.mdx` → https://docs.cline.bot/troubleshooting
|
||||
- Headings become anchors: `## Which Model` → `#which-model`
|
||||
|
||||
Search the `docs/` directory to find relevant documentation, then construct URLs to link users to:
|
||||
```bash
|
||||
ls docs/
|
||||
grep -r "keyword" docs/ --include="*.mdx" -l
|
||||
```
|
||||
|
||||
### Identify subject matter experts
|
||||
|
||||
For issues that clearly need engineering attention:
|
||||
git log --since="6 months ago" --format="%an" -- <relevant paths> | sort | uniq -c | sort -rn | head -5
|
||||
|
||||
Cross-reference with GitHub usernames. Include in your response (@mention, do NOT assign):
|
||||
|
||||
| SME | Reason |
|
||||
|-----|--------|
|
||||
| @username1 | Authored PR #X which modified this area |
|
||||
| @username2 | Primary contributor to affected file |
|
||||
|
||||
## Weak model detection
|
||||
|
||||
Many issues are caused by users running small or non-frontier models that don't tool-call reliably. Signs include:
|
||||
- Model failing to use tools correctly
|
||||
- Nonsensical or malformed responses
|
||||
- User is running a small/local model or older model version
|
||||
|
||||
If this looks like a weak model issue, kindly suggest they try reproducing with Claude Sonnet and report back if it persists. Link to https://docs.cline.bot/getting-started/selecting-your-model if helpful. Still label and triage normally.
|
||||
|
||||
## Your comment
|
||||
|
||||
Write a single comment as a helpful community member. Be conversational, not robotic. Include what's relevant:
|
||||
|
||||
- **Helpful response** - Answer their question, suggest a fix, provide a workaround. If you found solutions in related closed issues, surface those prominently.
|
||||
- **Duplicates and related issues** - Link to any you found and explain why they're duplicates/related. Summarize useful context from their comments.
|
||||
- **Regression analysis** - If this looks like a regression, explain what change likely caused it, link to the PR/commit, and tag the author.
|
||||
- **Clarifying questions** - If you need more info, ask specific questions. Don't ask for things already provided.
|
||||
- **SME table** - Include the table above if this needs engineering attention. Don't tag people for questions with obvious answers or weak-model issues.
|
||||
- **Context for maintainers** - Relevant code paths, what you found. Keep it concise.
|
||||
- **Docs links** - If there's relevant documentation, link to it naturally in your response as a recommendation (e.g., "For more details, check out [the Ollama setup guide](url)"). Do NOT add a "Sources" section at the end - integrate doc links into your response where they're helpful.
|
||||
- **Possible Duplicates section** - ALWAYS include a "Possible Duplicates" section at the end of your comment listing issues that might be duplicates so maintainers can quickly close if appropriate. If none found, say "No obvious duplicates found."
|
||||
|
||||
## Labels
|
||||
First, retrieve all available labels and read their descriptions to understand what each is for:
|
||||
gh label list --json name,description --limit 100
|
||||
|
||||
Then apply the appropriate labels based on your analysis. Only use labels from the list above—do not create new labels.
|
||||
gh issue edit ${{ github.event.issue.number || inputs.issue_number }} --add-label "label1,label2"
|
||||
|
||||
If your regression analysis found a likely culprit (a recent PR/commit that probably caused this issue), add the "Regression" label:
|
||||
gh issue edit ${{ github.event.issue.number || inputs.issue_number }} --add-label "Regression"
|
||||
|
||||
IMPORTANT: After posting your comment, add the "Bot Responded" label to indicate this issue has received an automated response:
|
||||
gh issue edit ${{ github.event.issue.number || inputs.issue_number }} --add-label "Bot Responded"
|
||||
|
||||
## Remember
|
||||
|
||||
- **This is a one-time automated response** - you will NOT see their reply or respond again. Never say things like "I can help you", "let me know", "once I have that info", or "I can give you more targeted help" - you won't be there to follow up. If you ask clarifying questions, frame them for the maintainers who will follow up, e.g., "If you can share X, that would help the maintainers diagnose this."
|
||||
- Don't be formulaic. Respond to what the issue actually needs.
|
||||
- Surface solutions from past issues - often the fastest path to helping.
|
||||
- Connecting regressions to specific changes is extremely valuable.
|
||||
- Link issues with #number so they're clickable.
|
||||
@@ -0,0 +1,272 @@
|
||||
name: Claude PR Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, ready_for_review]
|
||||
# Manual trigger for backfilling existing PRs. Run from terminal:
|
||||
# gh workflow run claude-pr-review.yml -f pr_number=1234
|
||||
# Or batch process open PRs:
|
||||
# gh pr list --state open --limit 10 --json number --jq '.[].number' | while read num; do
|
||||
# gh workflow run claude-pr-review.yml -f pr_number=$num
|
||||
# sleep 60
|
||||
# done
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to review'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
claude-pr-review:
|
||||
# Runs on PR opened/ready_for_review (skips drafts) or manual trigger for backfilling
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
|
||||
# SECURITY: These permissions are intentionally restrictive.
|
||||
# - contents: read -> Claude can read the codebase but CANNOT write/push any code
|
||||
# - pull-requests: write -> Claude can post reviews and inline suggestions
|
||||
# - issues: read -> Claude can search for related issues
|
||||
# NOTE: Even with pull-requests: write, Claude CANNOT merge PRs because branch protection
|
||||
# requires 1 approval from a Code Owner. The GITHUB_TOKEN cannot bypass this.
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get PR number
|
||||
id: pr
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "number=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Run PR Review
|
||||
id: review
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
allowed_non_write_users: "*"
|
||||
claude_args: --model claude-opus-4-5-20251101 --allowedTools "Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch"
|
||||
prompt: |
|
||||
You're a GitHub PR reviewer for the open source Cline repository. Your goal is to give the PR author helpful feedback and give maintainers the context they need to review efficiently.
|
||||
|
||||
PR: #${{ steps.pr.outputs.number }}
|
||||
|
||||
## Gather context
|
||||
|
||||
```bash
|
||||
# Get full PR details
|
||||
gh pr view ${{ steps.pr.outputs.number }} --json number,title,body,author,createdAt,updatedAt,isDraft,labels,commits,files,additions,deletions,changedFiles,baseRefName,headRefName,mergeable,reviewDecision
|
||||
|
||||
# Get the diff
|
||||
gh pr diff ${{ steps.pr.outputs.number }}
|
||||
|
||||
# Check CI status
|
||||
gh pr checks ${{ steps.pr.outputs.number }}
|
||||
|
||||
# Get existing review comments (to understand context and your previous feedback)
|
||||
gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/comments --jq '.[] | {user: .user.login, body: .body, path: .path, created_at: .created_at}'
|
||||
|
||||
# Get conversation comments
|
||||
gh pr view ${{ steps.pr.outputs.number }} --comments
|
||||
```
|
||||
|
||||
If this is a re-review (workflow_dispatch event):
|
||||
Read your previous comments carefully. Understand what you asked for before.
|
||||
Check if new commits or comments address your previous feedback.
|
||||
|
||||
## Check contributing guidelines
|
||||
|
||||
Flag (but don't block) if:
|
||||
- Missing changeset - For user-facing changes, check if there's a `.changeset/` file:
|
||||
```bash
|
||||
gh pr diff ${{ steps.pr.outputs.number }} --name-only | grep '.changeset/' || echo "No changeset found"
|
||||
```
|
||||
If missing, ask them to run `npm run changeset`
|
||||
- Missing tests - New features should have tests
|
||||
|
||||
## Find related issues and PRs
|
||||
|
||||
Search thoroughly for context that might help with the review:
|
||||
|
||||
```bash
|
||||
# Find related issues for context
|
||||
gh issue list --search "<keywords from the PR>" --state all --limit 30
|
||||
gh issue list --search "<error messages or feature names>" --state all --limit 20
|
||||
|
||||
# Find similar PRs for reference
|
||||
gh pr list --search "<keywords>" --state all --limit 30
|
||||
```
|
||||
|
||||
For each relevant issue or PR you find, read it including comments:
|
||||
```bash
|
||||
gh issue view <number> --comments
|
||||
gh pr view <number> --comments
|
||||
```
|
||||
|
||||
Look for:
|
||||
- Open issues this PR might fix that weren't linked in the description
|
||||
- Similar PRs that went through review - what feedback did they get? What patterns did they follow?
|
||||
- Context from maintainer discussions that could inform your review
|
||||
|
||||
## Find subject matter experts
|
||||
|
||||
For files changed in this PR, find who knows the code best:
|
||||
```bash
|
||||
# Get files changed
|
||||
gh pr diff ${{ steps.pr.outputs.number }} --name-only
|
||||
|
||||
# For each relevant path, find contributors
|
||||
git log --since="6 months ago" --format="%an" -- <path> | sort | uniq -c | sort -rn | head -5
|
||||
```
|
||||
|
||||
Cross-reference git authors with GitHub usernames. Include an SME table in your response:
|
||||
|
||||
| SME | Reason |
|
||||
|-----|--------|
|
||||
| @username1 | Authored PR #X which modified this area |
|
||||
| @username2 | Primary contributor to affected file (15 commits in 6 months) |
|
||||
| @username3 | Reviewed similar PR #Y with extensive feedback |
|
||||
|
||||
## Deep code review
|
||||
|
||||
This is the most important part. Don't just look for syntax issues - understand what the PR is trying to achieve and whether the implementation is the right approach.
|
||||
|
||||
Step 1: Understand the intent
|
||||
Read the PR description and understand what the author is trying to accomplish. What problem are they solving? What feature are they adding?
|
||||
|
||||
Step 2: Form your own opinion first
|
||||
Before analyzing their code, think about how YOU would implement this feature or fix. What files would you touch? What patterns would you follow? What edge cases would you handle?
|
||||
|
||||
Step 3: Compare approaches
|
||||
Now look at their implementation. How does it compare to what you would have done?
|
||||
- Is their approach better in some ways? Note what they did well.
|
||||
- Is their approach missing something? Be specific about what and why.
|
||||
- Are there edge cases they haven't considered?
|
||||
- Does it follow the patterns established in similar parts of the codebase?
|
||||
|
||||
Step 4: Look at the bigger picture
|
||||
- What other files or systems does this change interact with?
|
||||
- Could this break anything else?
|
||||
- Is there additional work needed beyond this PR to complete the feature?
|
||||
- Does this fit well with the overall architecture?
|
||||
|
||||
Step 5: Find reference implementations
|
||||
Look for similar changes in the codebase:
|
||||
```bash
|
||||
git log --oneline --all --grep="<relevant keywords>" | head -20
|
||||
git log --oneline -- <similar files> | head -20
|
||||
```
|
||||
|
||||
If this is adding a new API provider, look at how other providers are implemented.
|
||||
If this is adding a new feature, look at how similar features were added.
|
||||
Note where their implementation aligns with or diverges from established patterns.
|
||||
|
||||
Step 6: Standard code review checks
|
||||
- DRY: Is there duplicated code that could be extracted?
|
||||
- Error handling: Are errors handled appropriately?
|
||||
- Security: Any injection risks, credential exposure, unsafe dependencies?
|
||||
- Performance: Any obvious inefficiencies, memory leaks, N+1 patterns?
|
||||
- Types: Is TypeScript used correctly? Any unsafe type assertions?
|
||||
- Naming: Are variables and functions named clearly?
|
||||
- Comments: Is complex logic explained? Are there outdated comments?
|
||||
|
||||
## Inline code suggestions
|
||||
|
||||
For specific code improvements, use GitHub's suggestion syntax via `gh api`.
|
||||
This creates suggestions the author can commit with one click.
|
||||
|
||||
Single-line suggestion:
|
||||
```bash
|
||||
gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews \
|
||||
-X POST \
|
||||
-f commit_id="$(gh pr view ${{ steps.pr.outputs.number }} --json headRefOid -q .headRefOid)" \
|
||||
-f event="COMMENT" \
|
||||
-f body="" \
|
||||
-F comments='[
|
||||
{
|
||||
"path": "src/example.ts",
|
||||
"line": 42,
|
||||
"body": "Consider simplifying:\n\n```suggestion\nconst result = items.filter(Boolean);\n```"
|
||||
}
|
||||
]'
|
||||
```
|
||||
|
||||
Multi-line suggestion (replacing lines 40-45):
|
||||
```bash
|
||||
gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews \
|
||||
-X POST \
|
||||
-f commit_id="$(gh pr view ${{ steps.pr.outputs.number }} --json headRefOid -q .headRefOid)" \
|
||||
-f event="COMMENT" \
|
||||
-f body="" \
|
||||
-F comments='[
|
||||
{
|
||||
"path": "src/example.ts",
|
||||
"start_line": 40,
|
||||
"line": 45,
|
||||
"body": "This can be simplified:\n\n```suggestion\nconst simplified = doThing();\n```"
|
||||
}
|
||||
]'
|
||||
```
|
||||
|
||||
Use inline suggestions for concrete improvements. Use regular comments for questions or broader feedback.
|
||||
|
||||
## Post your review
|
||||
|
||||
After your investigation, post a single helpful comment that helps the author and gives maintainers context.
|
||||
|
||||
Start with a warm thank you for their contribution. Be conversational, not robotic.
|
||||
|
||||
Include what's relevant:
|
||||
- In-depth explanation of what the PR does - Be comprehensive. A maintainer should be able to read this section and fully understand the author's intent, why they made the changes, how they implemented it, and what files/systems are affected. Don't just summarize - explain.
|
||||
- Related issues/PRs you found that provide useful context (link to them)
|
||||
- Your review findings (issues to address, suggestions, etc.)
|
||||
- Clear next steps for the author
|
||||
|
||||
Include a "For Maintainers" section with:
|
||||
- Anything else useful to help the maintainer resolve this PR
|
||||
- Related issues/PRs with context on why they're relevant
|
||||
- Open issues this PR might fix that weren't linked in the description
|
||||
- Your recommendation: merge as-is, needs changes, needs discussion, close, etc.
|
||||
- SME table - who should review this and why
|
||||
|
||||
For the SME table:
|
||||
| SME | Reason |
|
||||
|-----|--------|
|
||||
| @username | Primary contributor to affected files |
|
||||
|
||||
## Update labels
|
||||
|
||||
Add appropriate labels based on your analysis:
|
||||
```bash
|
||||
gh label list --json name,description --limit 100
|
||||
gh pr edit ${{ steps.pr.outputs.number }} --add-label "label1,label2"
|
||||
```
|
||||
|
||||
When done, add the reviewed label:
|
||||
```bash
|
||||
gh pr edit ${{ steps.pr.outputs.number }} --add-label "Bot Reviewed"
|
||||
```
|
||||
|
||||
## Remember
|
||||
|
||||
- This is a one-time automated response - you will NOT see their reply or respond again. Never say things like "let me know if you have questions", "I can help you with", or "feel free to ask" - you won't be there to follow up. Frame any questions for the maintainers who will follow up.
|
||||
- Be helpful and welcoming - Many contributors are new to the project
|
||||
- Be specific - Point to exact lines and suggest fixes, don't give vague feedback
|
||||
- Think deeply - Don't just surface-level review, understand the intent and evaluate the approach
|
||||
- Use inline suggestions - Make it easy for authors to accept changes
|
||||
- You're a first-pass reviewer - A human maintainer will do final approval
|
||||
@@ -1,78 +0,0 @@
|
||||
name: Smoke Tests
|
||||
|
||||
# Temporarily disabled: this workflow built and linked the legacy CLI
|
||||
# (`cd cli && npm install && npm run build && npm link`) before running the
|
||||
# smoke-test scenarios. The legacy CLI publish chain has been retired in
|
||||
# favor of the SDK CLI at `sdk/apps/cli/`. The scenarios under
|
||||
# `evals/smoke-tests/scenarios/` are CLI-agnostic and should be re-enabled
|
||||
# once the build step is repointed at the new SDK CLI. Until then, only
|
||||
# manual `workflow_dispatch` runs are accepted (and will fail in their
|
||||
# current form).
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: smoke-tests-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
smoke-tests:
|
||||
name: Smoke Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build and install CLI
|
||||
run: |
|
||||
npm run protos
|
||||
cd cli && npm install && npm run build && npm link
|
||||
echo "$(npm config get prefix)/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Verify CLI
|
||||
run: cline --version
|
||||
|
||||
- name: Run smoke tests
|
||||
env:
|
||||
CLINE_API_KEY: ${{ secrets.CLINE_API_KEY }}
|
||||
run: |
|
||||
cline auth -p cline -k "$CLINE_API_KEY" -m "anthropic/claude-sonnet-4.5"
|
||||
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()
|
||||
run: cat evals/smoke-tests/results/latest/summary.md >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Upload results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: smoke-test-results-${{ github.run_id }}
|
||||
path: evals/smoke-tests/results/latest/
|
||||
retention-days: 30
|
||||
@@ -0,0 +1,312 @@
|
||||
name: Cline PR Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
[opened, ready_for_review]
|
||||
# Manual trigger for backfilling existing PRs. Run from terminal:
|
||||
# gh workflow run cline-pr-review.yml -f pr_number=1234
|
||||
# Or batch process open PRs:
|
||||
# gh pr list --state open --limit 10 --json number --jq '.[].number' | while read num; do
|
||||
# gh workflow run cline-pr-review.yml -f pr_number=$num
|
||||
# sleep 60
|
||||
# done
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: "PR number to review"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: pr-review-${{ github.event.pull_request.number || inputs.pr_number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
cline-pr-review:
|
||||
# Runs on PR opened/ready_for_review (skips drafts) or manual trigger for backfilling
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
# SECURITY: These permissions are intentionally restrictive.
|
||||
# - contents: read -> cline can read the codebase but CANNOT write/push any code
|
||||
# - pull-requests: write -> cline can post reviews and inline suggestions
|
||||
# - issues: read -> cline can search for related issues
|
||||
# NOTE: Even with pull-requests: write, cline CANNOT merge PRs because branch protection
|
||||
# requires 1 approval from a Code Owner. The GITHUB_TOKEN cannot bypass this.
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
|
||||
- name: Install and Verify Cline CLI
|
||||
run: |
|
||||
npx cline version # verify installation
|
||||
|
||||
- name: Configure Cline with Anthropic
|
||||
run: |
|
||||
npx cline auth --provider anthropic \
|
||||
--apikey "${{ secrets.ANTHROPIC_API_KEY }}" \
|
||||
--modelid claude-opus-4-5-20251101
|
||||
|
||||
- name: Get PR number
|
||||
id: pr
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "number=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Review PR with Cline
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.pr.outputs.number }}
|
||||
GITHUB_REPO: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
CLINE_COMMAND_PERMISSIONS: |
|
||||
{
|
||||
"allow": [
|
||||
"gh pr diff *",
|
||||
"gh pr view *",
|
||||
"gh pr checks *",
|
||||
"gh pr list *",
|
||||
"gh label list *",
|
||||
"gh issue list *",
|
||||
"gh issue view *",
|
||||
"git log *",
|
||||
"gh pr comment ${{ steps.pr.outputs.number }} *",
|
||||
"gh pr edit ${{ steps.pr.outputs.number }} *",
|
||||
"gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/comments *",
|
||||
"gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews *"
|
||||
]
|
||||
}
|
||||
run: |
|
||||
npx cline --yolo 'You'\''re a GitHub PR reviewer for the open source Cline repository. Your goal is to give the PR author helpful feedback and give maintainers the context they need to review efficiently.
|
||||
|
||||
PR: #'"${PR_NUMBER}"'
|
||||
|
||||
## Gather context
|
||||
|
||||
```bash
|
||||
# Get full PR details
|
||||
gh pr view '"${PR_NUMBER}"' --json number,title,body,author,createdAt,updatedAt,isDraft,labels,commits,files,additions,deletions,changedFiles,baseRefName,headRefName,mergeable,reviewDecision
|
||||
|
||||
# Get the diff
|
||||
gh pr diff '"${PR_NUMBER}"'
|
||||
|
||||
# Check CI status
|
||||
gh pr checks '"${PR_NUMBER}"'
|
||||
|
||||
# Get existing review comments (to understand context and your previous feedback)
|
||||
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/comments --jq '\''.[] | {user: .user.login, body: .body, path: .path, created_at: .created_at}'\''
|
||||
|
||||
# Get conversation comments
|
||||
gh pr view '"${PR_NUMBER}"' --comments
|
||||
```
|
||||
|
||||
If this is a re-review (workflow_dispatch event):
|
||||
Read your previous comments carefully. Understand what you asked for before.
|
||||
Check if new commits or comments address your previous feedback.
|
||||
|
||||
## Check contributing guidelines
|
||||
|
||||
Flag (but don'\''t block) if:
|
||||
- Missing changeset - For user-facing changes, check if there'\''s a `.changeset/` file:
|
||||
```bash
|
||||
gh pr diff '"${PR_NUMBER}"' --name-only | grep '\''.changeset/'\'' || echo '\''No changeset found'\''
|
||||
```
|
||||
If missing, ask them to run `npm run changeset`
|
||||
- Missing tests - New features should have tests
|
||||
|
||||
## Find related issues and PRs
|
||||
|
||||
Search thoroughly for context that might help with the review:
|
||||
|
||||
```bash
|
||||
# Find related issues for context
|
||||
gh issue list --search '\''<keywords from the PR>'\'' --state all --limit 30
|
||||
gh issue list --search '\''<error messages or feature names>'\'' --state all --limit 20
|
||||
|
||||
# Find similar PRs for reference
|
||||
gh pr list --search '\''<keywords>'\'' --state all --limit 30
|
||||
```
|
||||
|
||||
For each relevant issue or PR you find, read it including comments:
|
||||
```bash
|
||||
gh issue view <number> --comments
|
||||
gh pr view <number> --comments
|
||||
```
|
||||
|
||||
Look for:
|
||||
- Open issues this PR might fix that weren'\''t linked in the description
|
||||
- Similar PRs that went through review - what feedback did they get? What patterns did they follow?
|
||||
- Context from maintainer discussions that could inform your review
|
||||
|
||||
## Find subject matter experts
|
||||
|
||||
For files changed in this PR, find who knows the code best:
|
||||
```bash
|
||||
# Get files changed
|
||||
gh pr diff '"${PR_NUMBER}"' --name-only
|
||||
|
||||
# For each relevant path, find contributors
|
||||
git log --since='\''6 months ago'\'' --format='\''%an'\'' -- <path> | sort | uniq -c | sort -rn | head -5
|
||||
```
|
||||
|
||||
Cross-reference git authors with GitHub usernames. Include an SME table in your response:
|
||||
|
||||
| SME | Reason |
|
||||
|-----|--------|
|
||||
| @username1 | Authored PR #X which modified this area |
|
||||
| @username2 | Primary contributor to affected file (15 commits in 6 months) |
|
||||
| @username3 | Reviewed similar PR #Y with extensive feedback |
|
||||
|
||||
## Bash command usage
|
||||
|
||||
Don'\''t use operators like `|`, `&&`, or `;` - run each command separately and analyze the output.
|
||||
|
||||
When referencing command outputs, quote them properly to avoid formatting issues.
|
||||
|
||||
## Deep code review
|
||||
|
||||
This is the most important part. Don'\''t just look for syntax issues - understand what the PR is trying to achieve and whether the implementation is the right approach.
|
||||
|
||||
Step 1: Understand the intent
|
||||
Read the PR description and understand what the author is trying to accomplish. What problem are they solving? What feature are they adding?
|
||||
|
||||
Step 2: Form your own opinion first
|
||||
Before analyzing their code, think about how YOU would implement this feature or fix. What files would you touch? What patterns would you follow? What edge cases would you handle?
|
||||
|
||||
Step 3: Compare approaches
|
||||
Now look at their implementation. How does it compare to what you would have done?
|
||||
- Is their approach better in some ways? Note what they did well.
|
||||
- Is their approach missing something? Be specific about what and why.
|
||||
- Are there edge cases they haven'\''t considered?
|
||||
- Does it follow the patterns established in similar parts of the codebase?
|
||||
|
||||
Step 4: Look at the bigger picture
|
||||
- What other files or systems does this change interact with?
|
||||
- Could this break anything else?
|
||||
- Is there additional work needed beyond this PR to complete the feature?
|
||||
- Does this fit well with the overall architecture?
|
||||
|
||||
Step 5: Find reference implementations
|
||||
Look for similar changes in the codebase:
|
||||
```bash
|
||||
git log --oneline --all --grep='\''<relevant keywords>'\'' | head -20
|
||||
git log --oneline -- <similar files> | head -20
|
||||
```
|
||||
|
||||
If this is adding a new API provider, look at how other providers are implemented.
|
||||
If this is adding a new feature, look at how similar features were added.
|
||||
Note where their implementation aligns with or diverges from established patterns.
|
||||
|
||||
Step 6: Standard code review checks
|
||||
- DRY: Is there duplicated code that could be extracted?
|
||||
- Error handling: Are errors handled appropriately?
|
||||
- Security: Any injection risks, credential exposure, unsafe dependencies?
|
||||
- Performance: Any obvious inefficiencies, memory leaks, N+1 patterns?
|
||||
- Types: Is TypeScript used correctly? Any unsafe type assertions?
|
||||
- Naming: Are variables and functions named clearly?
|
||||
- Comments: Is complex logic explained? Are there outdated comments?
|
||||
|
||||
## Inline code suggestions
|
||||
|
||||
For specific code improvements, use GitHub'\''s suggestion syntax via `gh api`.
|
||||
This creates suggestions the author can commit with one click.
|
||||
|
||||
Single-line suggestion:
|
||||
```bash
|
||||
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/reviews \
|
||||
-X POST \
|
||||
-f commit_id="$(gh pr view '"${PR_NUMBER}"' --json headRefOid -q .headRefOid)" \
|
||||
-f event='\''COMMENT'\'' \
|
||||
-f body='\'''\'' \
|
||||
-F comments='\''[
|
||||
{
|
||||
"path": "src/example.ts",
|
||||
"line": 42,
|
||||
"body": "Consider simplifying:\n\n```suggestion\nconst result = items.filter(Boolean);\n```"
|
||||
}
|
||||
]'\''
|
||||
```
|
||||
|
||||
Multi-line suggestion (replacing lines 40-45):
|
||||
```bash
|
||||
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/reviews \
|
||||
-X POST \
|
||||
-f commit_id="$(gh pr view '"${PR_NUMBER}"' --json headRefOid -q .headRefOid)" \
|
||||
-f event='\''COMMENT'\'' \
|
||||
-f body='\'''\'' \
|
||||
-F comments='\''[
|
||||
{
|
||||
"path": "src/example.ts",
|
||||
"start_line": 40,
|
||||
"line": 45,
|
||||
"body": "This can be simplified:\n\n```suggestion\nconst simplified = doThing();\n```"
|
||||
}
|
||||
]'\''
|
||||
```
|
||||
|
||||
Use inline suggestions for concrete improvements. Use regular comments for questions or broader feedback.
|
||||
|
||||
## Post your review
|
||||
|
||||
After your investigation, post a single helpful comment that helps the author and gives maintainers context.
|
||||
|
||||
Start with a warm thank you for their contribution. Be conversational, not robotic.
|
||||
|
||||
Include what'\''s relevant:
|
||||
- In-depth explanation of what the PR does - Be comprehensive. A maintainer should be able to read this section and fully understand the author'\''s intent, why they made the changes, how they implemented it, and what files/systems are affected. Don'\''t just summarize - explain.
|
||||
- Related issues/PRs you found that provide useful context (link to them)
|
||||
- Your review findings (issues to address, suggestions, etc.)
|
||||
- Clear next steps for the author
|
||||
|
||||
Include a '\''For Maintainers'\'' section with:
|
||||
- Anything else useful to help the maintainer resolve this PR
|
||||
- Related issues/PRs with context on why they'\''re relevant
|
||||
- Open issues this PR might fix that weren'\''t linked in the description
|
||||
- Your recommendation: merge as-is, needs changes, needs discussion, close, etc.
|
||||
- SME table - who should review this and why
|
||||
|
||||
For the SME table:
|
||||
| SME | Reason |
|
||||
|-----|--------|
|
||||
| @username | Primary contributor to affected files |
|
||||
|
||||
## Update labels
|
||||
|
||||
Add appropriate labels based on your analysis:
|
||||
```bash
|
||||
gh label list --json name,description --limit 100
|
||||
gh pr edit '"${PR_NUMBER}"' --add-label '\''label1,label2'\''
|
||||
```
|
||||
|
||||
When done, add the reviewed label:
|
||||
```bash
|
||||
gh pr edit '"${PR_NUMBER}"' --add-label '\''Bot Reviewed'\''
|
||||
```
|
||||
|
||||
## Remember
|
||||
|
||||
- This is a one-time automated response - you will NOT see their reply or respond again. Never say things like '\''let me know if you have questions'\'', '\''I can help you with'\'', or '\''feel free to ask'\'' - you won'\''t be there to follow up. Frame any questions for the maintainers who will follow up.
|
||||
- Be helpful and welcoming - Many contributors are new to the project
|
||||
- Be specific - Point to exact lines and suggest fixes, don'\''t give vague feedback
|
||||
- Think deeply - Don'\''t just surface-level review, understand the intent and evaluate the approach
|
||||
- Use inline suggestions - Make it easy for authors to accept changes
|
||||
- You'\''re a first-pass reviewer - A human maintainer will do final approval'
|
||||
@@ -80,16 +80,13 @@ jobs:
|
||||
playwright-browsers-${{ runner.os }}-
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Install vsce
|
||||
run: npm install -g @vscode/vsce
|
||||
|
||||
- name: Install xvfb on Linux
|
||||
if: matrix.runner == 'ubuntu'
|
||||
run: sudo apt-get update && sudo apt-get install -y xvfb
|
||||
|
||||
@@ -51,15 +51,3 @@ jobs:
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check if beta version checkbox is checked
|
||||
if (body.includes('- [X] I am using a beta version of Cline') || body.includes('- [x] I am using a beta version of Cline')) {
|
||||
if (!labels.includes('beta')) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['beta']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
name: Publish NPM Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
confirm_publish:
|
||||
description: 'Type "publish" to confirm you want to publish to NPM'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write # Required by test workflow
|
||||
pull-requests: write # Required by test workflow
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yml
|
||||
|
||||
publish-npm-release:
|
||||
needs: test
|
||||
name: Publish Cline CLI to NPM
|
||||
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main' && github.event.inputs.confirm_publish == 'publish'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
# Cache root dependencies
|
||||
- name: Cache root dependencies
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
# Cache cli-ts dependencies
|
||||
- name: Cache cli-ts dependencies
|
||||
uses: actions/cache@v4
|
||||
id: cli-ts-cache
|
||||
with:
|
||||
path: cli-ts/node_modules
|
||||
key: ${{ runner.os }}-npm-cli-ts-${{ hashFiles('cli-ts/package-lock.json') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci --include=optional
|
||||
|
||||
- name: Install cli-ts dependencies
|
||||
if: steps.cli-ts-cache.outputs.cache-hit != 'true'
|
||||
run: cd cli-ts && npm ci
|
||||
|
||||
- name: Generate Protos
|
||||
run: npm run protos
|
||||
|
||||
- name: Read release version
|
||||
id: version
|
||||
run: |
|
||||
# Read version from cli-ts/package.json
|
||||
VERSION=$(node -p "require('./cli-ts/package.json').version")
|
||||
echo "Release version: $VERSION"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Clean previous builds
|
||||
run: rm -rf dist-standalone
|
||||
|
||||
- name: Build and package CLI
|
||||
env:
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
CLINE_ENVIRONMENT: production
|
||||
OTEL_TELEMETRY_ENABLED: "1"
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
POSTHOG_TELEMETRY_ENABLED: "true"
|
||||
run: node scripts/package-npm.mjs
|
||||
|
||||
- name: Verify build output
|
||||
run: |
|
||||
echo "Checking dist-standalone directory..."
|
||||
ls -la dist-standalone/
|
||||
|
||||
echo "Checking dist-standalone/dist..."
|
||||
ls -la dist-standalone/dist/
|
||||
|
||||
echo "Checking package.json in dist-standalone..."
|
||||
cat dist-standalone/package.json
|
||||
|
||||
- name: Publish to NPM with latest tag
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_RELEASE_TOKEN }}
|
||||
run: |
|
||||
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'latest'..."
|
||||
cd dist-standalone
|
||||
npm publish --tag latest --access public
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "✅ Successfully published cline@${{ steps.version.outputs.version }} to NPM with tag 'latest'"
|
||||
echo ""
|
||||
echo "📦 Install with: npm install -g cline"
|
||||
echo "🔗 NPM: https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}"
|
||||
@@ -0,0 +1,152 @@
|
||||
name: Publish NPM Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 12 * * *' # 4 AM PST (UTC-8) = 12 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write # Required by test workflow
|
||||
pull-requests: write # Required by test workflow
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yml
|
||||
|
||||
publish-npm-nightly:
|
||||
needs: test
|
||||
name: Publish Cline CLI (Nightly) to NPM
|
||||
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for recent commits
|
||||
id: check_commits
|
||||
run: |
|
||||
if [ $(git rev-list --count HEAD --since="24 hours ago") -eq 0 ]; then
|
||||
echo "No commits in last 24 hours, skipping publish"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Found recent commits, proceeding with publish"
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
# Cache root dependencies
|
||||
- name: Cache root dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
# Cache cli-ts dependencies
|
||||
- name: Cache cli-ts dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/cache@v4
|
||||
id: cli-ts-cache
|
||||
with:
|
||||
path: cli-ts/node_modules
|
||||
key: ${{ runner.os }}-npm-cli-ts-${{ hashFiles('cli-ts/package-lock.json') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true' && steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci --include=optional
|
||||
|
||||
- name: Install cli-ts dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true' && steps.cli-ts-cache.outputs.cache-hit != 'true'
|
||||
run: cd cli-ts && npm ci
|
||||
|
||||
- name: Generate Protos
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: npm run protos
|
||||
|
||||
- name: Generate nightly version with timestamp
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
id: version
|
||||
run: |
|
||||
# Read base version from cli-ts/package.json (e.g., "2.0.0")
|
||||
BASE_VERSION=$(node -p "require('./cli-ts/package.json').version")
|
||||
|
||||
# Generate timestamp (Unix epoch seconds)
|
||||
TIMESTAMP=$(date +%s)
|
||||
|
||||
# Create unique nightly version: 2.0.0-nightly.1736365200
|
||||
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
|
||||
|
||||
echo "Base version: $BASE_VERSION"
|
||||
echo "Generated nightly version: $VERSION"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update cli-ts/package.json with nightly version
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
# Update version with timestamp-based nightly version
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('cli-ts/package.json', 'utf8'));
|
||||
pkg.version = '${{ steps.version.outputs.version }}';
|
||||
fs.writeFileSync('cli-ts/package.json', JSON.stringify(pkg, null, '\t'));
|
||||
"
|
||||
|
||||
echo "Using version ${{ steps.version.outputs.version }} for build"
|
||||
cat cli-ts/package.json | grep '"version"'
|
||||
|
||||
- name: Clean previous builds
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: rm -rf dist-standalone
|
||||
|
||||
- name: Build and package CLI
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
CLINE_ENVIRONMENT: production
|
||||
OTEL_TELEMETRY_ENABLED: "1"
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
POSTHOG_TELEMETRY_ENABLED: "true"
|
||||
run: node scripts/package-npm.mjs
|
||||
|
||||
- name: Verify build output
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
echo "Checking dist-standalone directory..."
|
||||
ls -la dist-standalone/
|
||||
|
||||
echo "Checking dist-standalone/dist..."
|
||||
ls -la dist-standalone/dist/
|
||||
|
||||
echo "Checking package.json in dist-standalone..."
|
||||
cat dist-standalone/package.json
|
||||
|
||||
- name: Publish to NPM with nightly tag
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_RELEASE_TOKEN }}
|
||||
run: |
|
||||
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'nightly'..."
|
||||
cd dist-standalone
|
||||
npm publish --tag nightly --access public
|
||||
|
||||
- name: Summary
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
echo "✅ Successfully published cline@${{ steps.version.outputs.version }} to NPM with tag 'nightly'"
|
||||
echo ""
|
||||
echo "📦 Install with: npm install -g cline@nightly"
|
||||
echo "🔗 NPM: https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}"
|
||||
@@ -1,394 +0,0 @@
|
||||
name: Publish CLI to NPM
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 12 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish_target:
|
||||
description: "Which publish flow to run"
|
||||
required: true
|
||||
default: "main"
|
||||
type: choice
|
||||
options:
|
||||
- main
|
||||
- nightly
|
||||
git_tag:
|
||||
description: "Existing release tag to publish when publish_target=main, for example cli-v0.1.0"
|
||||
required: false
|
||||
type: string
|
||||
confirm_publish:
|
||||
description: 'Required when publish_target=main. Type "publish" to confirm release publish.'
|
||||
required: false
|
||||
type: string
|
||||
force_nightly_publish:
|
||||
description: "Force nightly publish even with no commits in last 24h"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: sdk
|
||||
|
||||
jobs:
|
||||
publish-main:
|
||||
name: Publish cline
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
if: |
|
||||
github.repository == 'cline/cline' &&
|
||||
github.ref == 'refs/heads/main' &&
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
github.event.inputs.publish_target == 'main' &&
|
||||
github.event.inputs.confirm_publish == 'publish' &&
|
||||
!endsWith(github.actor, '[bot]')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.git_tag }}
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.3.13"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Verify publish tooling
|
||||
run: |
|
||||
NPM_VERSION=$(npm --version)
|
||||
echo "npm ${NPM_VERSION}"
|
||||
IFS=. read -r major minor patch <<EOF
|
||||
${NPM_VERSION}
|
||||
EOF
|
||||
if [ "$major" -lt 11 ] || { [ "$major" -eq 11 ] && [ "$minor" -lt 5 ]; } || { [ "$major" -eq 11 ] && [ "$minor" -eq 5 ] && [ "$patch" -lt 1 ]; }; then
|
||||
echo "npm 11.5.1 or newer is required for trusted publishing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Validate release tag
|
||||
id: version
|
||||
run: |
|
||||
TAG="${{ github.event.inputs.git_tag }}"
|
||||
|
||||
if [ -z "$TAG" ]; then
|
||||
echo "git_tag is required when publish_target=main"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! printf "%s\n" "$TAG" | grep -Eq '^cli-v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
|
||||
echo "git_tag must look like cli-vX.Y.Z, got: ${TAG}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="${TAG#cli-v}"
|
||||
PACKAGE_VERSION=$(node -p "require('./apps/cli/package.json').version")
|
||||
|
||||
if [ "$PACKAGE_VERSION" != "$VERSION" ]; then
|
||||
echo "sdk/apps/cli/package.json version ${PACKAGE_VERSION} does not match ${TAG}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! printf "%s\n" "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
|
||||
echo "sdk/apps/cli/package.json has invalid version: ${VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TAG_COMMIT=$(git rev-parse "${TAG}^{commit}")
|
||||
HEAD_COMMIT=$(git rev-parse HEAD)
|
||||
if [ "$TAG_COMMIT" != "$HEAD_COMMIT" ]; then
|
||||
echo "${TAG} does not point at the checked out commit"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git fetch origin +main:refs/remotes/origin/main
|
||||
if ! git merge-base --is-ancestor "$HEAD_COMMIT" origin/main; then
|
||||
echo "${TAG} is not reachable from origin/main"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build SDK packages
|
||||
run: bun run build:sdk
|
||||
|
||||
- name: Run tests
|
||||
run: bun run test
|
||||
|
||||
- name: Build platform binaries
|
||||
run: bun script/build.ts --install-native-variants --skip-sdk-build
|
||||
working-directory: sdk/apps/cli
|
||||
|
||||
- name: Verify build output
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
EXPECTED=(
|
||||
"@cline/cli-darwin-arm64"
|
||||
"@cline/cli-darwin-x64"
|
||||
"@cline/cli-linux-arm64"
|
||||
"@cline/cli-linux-x64"
|
||||
"@cline/cli-windows-arm64"
|
||||
"@cline/cli-windows-x64"
|
||||
)
|
||||
|
||||
for package_name in "${EXPECTED[@]}"; do
|
||||
dir="apps/cli/dist/${package_name#@cline/}"
|
||||
if [ ! -f "$dir/package.json" ]; then
|
||||
echo "Missing package manifest: $dir/package.json"
|
||||
exit 1
|
||||
fi
|
||||
actual_name=$(node -p "require('./$dir/package.json').name")
|
||||
actual_version=$(node -p "require('./$dir/package.json').version")
|
||||
if [ "$actual_name" != "$package_name" ]; then
|
||||
echo "Expected $package_name, got $actual_name"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$actual_version" != "$VERSION" ]; then
|
||||
echo "Expected $package_name@$VERSION, got $actual_version"
|
||||
exit 1
|
||||
fi
|
||||
ls -lh "$dir/bin/"
|
||||
done
|
||||
|
||||
- name: Publish to NPM with latest tag
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
run: bun script/publish-npm.ts --tag latest
|
||||
working-directory: sdk/apps/cli
|
||||
|
||||
- name: Get Previous CLI Tag
|
||||
id: prev_tag
|
||||
run: |
|
||||
CURRENT_TAG="${{ steps.version.outputs.tag }}"
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 --match 'cli-v*' "$CURRENT_TAG^" 2>/dev/null || echo "")
|
||||
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get Changelog Entry
|
||||
id: changelog
|
||||
run: |
|
||||
# Grab content between the first "## " header and the next one in apps/cli/CHANGELOG.md
|
||||
CONTENT=$(awk '/^## [0-9]/{if(found) exit; found=1; next} found{print}' apps/cli/CHANGELOG.md)
|
||||
echo "content<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$CONTENT" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ steps.version.outputs.tag }}
|
||||
name: "CLI v${{ steps.version.outputs.version }}"
|
||||
body: |
|
||||
${{ steps.changelog.outputs.content }}
|
||||
${{ steps.prev_tag.outputs.prev_tag != '' && format('Full Changelog: https://github.com/{0}/compare/{1}...{2}', github.repository, steps.prev_tag.outputs.prev_tag, steps.version.outputs.tag) || '' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
echo "Published cline@${VERSION} to npm with dist-tag 'latest'"
|
||||
echo "Install with: npm install -g cline"
|
||||
|
||||
- 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: "section"
|
||||
text:
|
||||
type: "mrkdwn"
|
||||
text: ${{ toJSON(steps.changelog.outputs.content) }}
|
||||
- type: "context"
|
||||
elements:
|
||||
- type: "mrkdwn"
|
||||
text: "<https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}|View on npm>${{ steps.prev_tag.outputs.prev_tag != '' && format(' | Full Changelog: https://github.com/{0}/compare/{1}...{2}', github.repository, steps.prev_tag.outputs.prev_tag, steps.version.outputs.tag) || '' }}"
|
||||
|
||||
publish-nightly:
|
||||
name: Publish cline nightly
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
if: |
|
||||
github.repository == 'cline/cline' &&
|
||||
github.ref == 'refs/heads/main' &&
|
||||
(
|
||||
github.event_name == 'schedule' ||
|
||||
(
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
github.event.inputs.publish_target == 'nightly'
|
||||
)
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check for recent commits
|
||||
id: check_commits
|
||||
env:
|
||||
FORCE_PUBLISH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_nightly_publish == 'true' }}
|
||||
run: |
|
||||
if [ "$FORCE_PUBLISH" = "true" ]; then
|
||||
echo "force_nightly_publish enabled, proceeding with publish"
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$(git rev-list --count HEAD --since='24 hours ago')" -eq 0 ]; then
|
||||
echo "No commits in last 24 hours, skipping publish"
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Found recent commits, proceeding with publish"
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Setup Bun
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.3.13"
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Verify publish tooling
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
NPM_VERSION=$(npm --version)
|
||||
echo "npm ${NPM_VERSION}"
|
||||
IFS=. read -r major minor patch <<EOF
|
||||
${NPM_VERSION}
|
||||
EOF
|
||||
if [ "$major" -lt 11 ] || { [ "$major" -eq 11 ] && [ "$minor" -lt 5 ]; } || { [ "$major" -eq 11 ] && [ "$minor" -eq 5 ] && [ "$patch" -lt 1 ]; }; then
|
||||
echo "npm 11.5.1 or newer is required for trusted publishing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: bun install
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build SDK packages
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: bun run build:sdk
|
||||
|
||||
- name: Run tests
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: bun run test
|
||||
|
||||
- name: Generate nightly version
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
id: version
|
||||
run: |
|
||||
BASE_VERSION=$(node -p "require('./apps/cli/package.json').version")
|
||||
TIMESTAMP=$(date +%s)
|
||||
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
|
||||
|
||||
echo "Base version: ${BASE_VERSION}"
|
||||
echo "Generated nightly version: ${VERSION}"
|
||||
echo "base_version=${BASE_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Update nightly package version
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
node -e '
|
||||
const fs = require("node:fs");
|
||||
const path = "apps/cli/package.json";
|
||||
const pkg = JSON.parse(fs.readFileSync(path, "utf8"));
|
||||
pkg.version = process.env.VERSION;
|
||||
fs.writeFileSync(path, `${JSON.stringify(pkg, null, "\t")}\n`);
|
||||
'
|
||||
cat apps/cli/package.json | grep '"version"'
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
|
||||
- name: Build platform binaries
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: bun script/build.ts --install-native-variants --skip-sdk-build
|
||||
working-directory: sdk/apps/cli
|
||||
|
||||
- name: Verify build output
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
EXPECTED=(
|
||||
"@cline/cli-darwin-arm64"
|
||||
"@cline/cli-darwin-x64"
|
||||
"@cline/cli-linux-arm64"
|
||||
"@cline/cli-linux-x64"
|
||||
"@cline/cli-windows-arm64"
|
||||
"@cline/cli-windows-x64"
|
||||
)
|
||||
|
||||
for package_name in "${EXPECTED[@]}"; do
|
||||
dir="apps/cli/dist/${package_name#@cline/}"
|
||||
if [ ! -f "$dir/package.json" ]; then
|
||||
echo "Missing package manifest: $dir/package.json"
|
||||
exit 1
|
||||
fi
|
||||
actual_name=$(node -p "require('./$dir/package.json').name")
|
||||
actual_version=$(node -p "require('./$dir/package.json').version")
|
||||
if [ "$actual_name" != "$package_name" ]; then
|
||||
echo "Expected $package_name, got $actual_name"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$actual_version" != "$VERSION" ]; then
|
||||
echo "Expected $package_name@$VERSION, got $actual_version"
|
||||
exit 1
|
||||
fi
|
||||
ls -lh "$dir/bin/"
|
||||
done
|
||||
|
||||
- name: Publish to NPM with nightly tag
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
run: bun script/publish-npm.ts --tag nightly
|
||||
working-directory: sdk/apps/cli
|
||||
|
||||
- name: Summary
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
echo "Published cline@${VERSION} to npm with dist-tag 'nightly'"
|
||||
echo "Install with: npm install -g cline@nightly"
|
||||
@@ -1,72 +0,0 @@
|
||||
name: "Publish New SDK Extension Nightly"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 12 * * *' # 4 AM PST (UTC-8) = 12 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
# Keep the publish source pinned to one reviewed branch instead of accepting arbitrary refs.
|
||||
SDK_NIGHTLY_REF: dpc/sdk-migration-simpler-login
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish Cline New SDK Extension Nightly
|
||||
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
environment: PublishNightly
|
||||
|
||||
steps:
|
||||
- name: Checkout trusted SDK nightly branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ env.SDK_NIGHTLY_REF }}
|
||||
lfs: true
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
# 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
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci --include=optional
|
||||
|
||||
- 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 SDK nightly extension
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||
OVSX_PAT: ${{ secrets.OVSX_PAT }}
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
CLINE_ENVIRONMENT: production
|
||||
# OpenTelemetry production defaults (can be overridden at runtime)
|
||||
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
run: npm run publish:marketplace:nightly
|
||||
@@ -1,105 +1,82 @@
|
||||
name: "Publish Nightly Release"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 12 * * *' # 4 AM PST (UTC-8) = 12 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
run-name: "Publish Nightly from ${{ github.ref_name }} @ ${{ github.sha }}"
|
||||
|
||||
# Prevent concurrent publish runs on the same branch. The nightly publish script
|
||||
# generates the extension version from a seconds-resolution timestamp, so parallel
|
||||
# runs on the same ref can collide on the same version and cause publish failures
|
||||
# or inconsistent tagging. Runs on different branches proceed independently.
|
||||
concurrency:
|
||||
group: publish-nightly-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions: {}
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
test:
|
||||
if: github.repository == 'cline/cline' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dpc/sdk-migration-simpler-login')
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/test.yml
|
||||
test:
|
||||
uses: ./.github/workflows/test.yml
|
||||
|
||||
publish:
|
||||
needs: test
|
||||
permissions:
|
||||
contents: write
|
||||
name: Publish Cline (Nightly) Extension
|
||||
if: github.repository == 'cline/cline' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dpc/sdk-migration-simpler-login')
|
||||
runs-on: ubuntu-latest
|
||||
environment: PublishNightly
|
||||
publish:
|
||||
needs: test
|
||||
name: Publish Cline (Nightly) Extension
|
||||
if: github.repository == 'cline/cline'
|
||||
runs-on: ubuntu-latest
|
||||
environment: PublishNightly
|
||||
|
||||
steps:
|
||||
- name: Checkout selected branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
lfs: true
|
||||
persist-credentials: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Show build source
|
||||
run: |
|
||||
echo "Building ref: $GITHUB_REF"
|
||||
echo "Building sha: $GITHUB_SHA"
|
||||
git --no-pager log -1 --oneline
|
||||
- name: Check for recent commits
|
||||
run: |
|
||||
if [ $(git rev-list --count HEAD --since="24 hours ago") -eq 0 ]; then
|
||||
echo "No commits in last 24 hours, exiting"
|
||||
exit 0
|
||||
fi
|
||||
echo "Found recent commits, proceeding with build"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
# 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
|
||||
# Cache root dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache root dependencies
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci --include=optional
|
||||
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache webview-ui dependencies
|
||||
uses: actions/cache@v4
|
||||
id: webview-cache
|
||||
with:
|
||||
path: webview-ui/node_modules
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci --include=optional
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci --include=optional
|
||||
|
||||
- name: Install Publishing Tools
|
||||
run: npm install -g @vscode/vsce ovsx
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci --include=optional
|
||||
|
||||
- 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: Install Publishing Tools
|
||||
run: npm install -g @vscode/vsce ovsx
|
||||
|
||||
- name: Publish Nightly Extension
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||
OVSX_PAT: ${{ secrets.OVSX_PAT }}
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
CLINE_ENVIRONMENT: production
|
||||
# OpenTelemetry production defaults (can be overridden at runtime)
|
||||
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
run: npm run publish:marketplace:nightly
|
||||
|
||||
- name: Tag published commit
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
SAFE_REF=$(echo "$GITHUB_REF_NAME" | tr '/[:upper:]' '-[:lower:]' | tr -cd 'a-z0-9._-')
|
||||
SHORT_SHA=$(git rev-parse --short=12 HEAD)
|
||||
TIMESTAMP=$(date -u +"%Y%m%d%H%M%S")
|
||||
TAG="nightly-${SAFE_REF}-${TIMESTAMP}-${SHORT_SHA}"
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag -a "$TAG" -m "Cline Nightly published from ${GITHUB_REF_NAME} at ${GITHUB_SHA}"
|
||||
# Use an explicit HTTPS remote with GH_TOKEN because checkout was run with
|
||||
# persist-credentials: false, so actions/checkout did not persist a git credential helper.
|
||||
git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "refs/tags/${TAG}"
|
||||
|
||||
echo "Tagged published commit: $TAG"
|
||||
- name: Publish Extension as Pre-release
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||
OVSX_PAT: ${{ secrets.OVSX_PAT }}
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
CLINE_ENVIRONMENT: production
|
||||
# OpenTelemetry production defaults (can be overridden at runtime)
|
||||
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
run: npm run publish:marketplace:nightly
|
||||
|
||||
@@ -1,269 +0,0 @@
|
||||
name: Publish Main SDK Packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
channel:
|
||||
description: "Publish channel"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- nightly
|
||||
- latest
|
||||
default: nightly
|
||||
force_publish:
|
||||
description: "Force publish even if there are no commits in the last 24 hours"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
confirm_publish:
|
||||
description: 'Required when channel=latest. Type "publish" to confirm release publish.'
|
||||
required: false
|
||||
type: string
|
||||
schedule:
|
||||
# Run nightly at 2:00 AM UTC
|
||||
- cron: "0 2 * * *"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: sdk
|
||||
|
||||
jobs:
|
||||
test:
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/sdk-test.yml
|
||||
|
||||
publish-sdk:
|
||||
needs: test
|
||||
name: Publish SDK Packages
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
if: |
|
||||
github.repository == 'cline/cline' &&
|
||||
github.ref == 'refs/heads/main' &&
|
||||
(
|
||||
github.event_name != 'workflow_dispatch' ||
|
||||
inputs.channel != 'latest' ||
|
||||
(
|
||||
inputs.confirm_publish == 'publish' &&
|
||||
!endsWith(github.actor, '[bot]')
|
||||
)
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Determine publish channel
|
||||
id: channel
|
||||
run: |
|
||||
# Default to nightly for scheduled runs
|
||||
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||||
echo "channel=nightly" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "channel=${{ inputs.channel }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check for recent commits
|
||||
id: check_commits
|
||||
run: |
|
||||
CHANNEL="${{ steps.channel.outputs.channel }}"
|
||||
|
||||
# Always publish for latest (production) releases
|
||||
if [ "$CHANNEL" = "latest" ]; then
|
||||
echo "Production release requested, proceeding with publish"
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${{ inputs.force_publish }}" = "true" ]; then
|
||||
echo "force_publish enabled, proceeding with publish"
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$(git rev-list --count HEAD --since="24 hours ago")" -eq 0 ]; then
|
||||
echo "No commits in last 24 hours, skipping publish"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Found recent commits, proceeding with publish"
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Verify trusted publishing context
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then
|
||||
echo "GitHub OIDC request environment is unavailable. Ensure this job has id-token: write for npm trusted publishing."
|
||||
exit 1
|
||||
fi
|
||||
echo "GitHub OIDC request environment is available for npm trusted publishing."
|
||||
|
||||
- name: Setup Bun
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.3.13"
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Verify publish tooling
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
NPM_VERSION=$(npm --version)
|
||||
echo "npm ${NPM_VERSION}"
|
||||
IFS=. read -r major minor patch <<EOF
|
||||
${NPM_VERSION}
|
||||
EOF
|
||||
if [ "$major" -lt 11 ] || { [ "$major" -eq 11 ] && [ "$minor" -lt 5 ]; } || { [ "$major" -eq 11 ] && [ "$minor" -eq 5 ] && [ "$patch" -lt 1 ]; }; then
|
||||
echo "npm 11.5.1 or newer is required for trusted publishing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: bun install
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build SDK
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: bun run build:sdk
|
||||
|
||||
- name: Generate shared version
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
id: version
|
||||
run: |
|
||||
CHANNEL="${{ steps.channel.outputs.channel }}"
|
||||
BASE_VERSION=$(node -p "require('./packages/llms/package.json').version")
|
||||
|
||||
if [ "$CHANNEL" = "nightly" ]; then
|
||||
TIMESTAMP=$(date +%s)
|
||||
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
|
||||
else
|
||||
VERSION="$BASE_VERSION"
|
||||
fi
|
||||
|
||||
echo "Base version: $BASE_VERSION"
|
||||
echo "Channel: $CHANNEL"
|
||||
echo "Publish version: $VERSION"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update all package versions and lockfile
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: bun scripts/version.ts "${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Verify publishability
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: bun scripts/check-publish.ts
|
||||
|
||||
- name: Prepare package tarball directory
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: mkdir -p "$RUNNER_TEMP/sdk-npm-packs"
|
||||
|
||||
# Pack with Bun so workspace/catalog protocols are resolved in the tarball,
|
||||
# then publish that tarball with npm so npm trusted publishing can use GitHub OIDC.
|
||||
# Publish sequentially in dependency order: shared → llms → agents → core → sdk
|
||||
- name: Publish @cline/shared
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
run: |
|
||||
CHANNEL="${{ steps.channel.outputs.channel }}"
|
||||
echo "Publishing @cline/shared@${{ steps.version.outputs.version }} with tag '${CHANNEL}'..."
|
||||
cd packages/shared
|
||||
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
|
||||
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
|
||||
|
||||
- name: Publish @cline/llms
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
run: |
|
||||
CHANNEL="${{ steps.channel.outputs.channel }}"
|
||||
echo "Publishing @cline/llms@${{ steps.version.outputs.version }} with tag '${CHANNEL}'..."
|
||||
cd packages/llms
|
||||
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
|
||||
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
|
||||
|
||||
- name: Publish @cline/agents
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
run: |
|
||||
CHANNEL="${{ steps.channel.outputs.channel }}"
|
||||
echo "Publishing @cline/agents@${{ steps.version.outputs.version }} with tag '${CHANNEL}'..."
|
||||
cd packages/agents
|
||||
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
|
||||
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
|
||||
|
||||
- name: Publish @cline/core
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
run: |
|
||||
CHANNEL="${{ steps.channel.outputs.channel }}"
|
||||
echo "Publishing @cline/core@${{ steps.version.outputs.version }} with tag '${CHANNEL}'..."
|
||||
cd packages/core
|
||||
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
|
||||
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
|
||||
|
||||
- name: Publish @cline/sdk
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
run: |
|
||||
CHANNEL="${{ steps.channel.outputs.channel }}"
|
||||
echo "Publishing @cline/sdk@${{ steps.version.outputs.version }} with tag '${CHANNEL}'..."
|
||||
cd packages/sdk
|
||||
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
|
||||
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
|
||||
|
||||
- name: Create package tags for production publish
|
||||
if: steps.check_commits.outputs.skip != 'true' && steps.channel.outputs.channel == 'latest'
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
for PKG in shared llms agents core sdk; do
|
||||
TAG="sdk/${PKG}/v${VERSION}"
|
||||
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
|
||||
echo "Tag already exists locally: ${TAG}"
|
||||
else
|
||||
git tag -a "${TAG}" -m "@cline/${PKG}@${VERSION}"
|
||||
echo "Created tag: ${TAG}"
|
||||
fi
|
||||
|
||||
# Ensure remote has the tag; this is idempotent if tag already exists remotely.
|
||||
git push origin "refs/tags/${TAG}"
|
||||
done
|
||||
|
||||
- name: Summary
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
CHANNEL="${{ steps.channel.outputs.channel }}"
|
||||
echo "Published SDK packages with tag '${CHANNEL}':"
|
||||
echo " - @cline/shared@${VERSION}"
|
||||
echo " - @cline/llms@${VERSION}"
|
||||
echo " - @cline/agents@${VERSION}"
|
||||
echo " - @cline/core@${VERSION}"
|
||||
echo " - @cline/sdk@${VERSION}"
|
||||
if [ "$CHANNEL" = "latest" ]; then
|
||||
echo "Created git tags:"
|
||||
echo " - sdk/shared/v${VERSION}"
|
||||
echo " - sdk/llms/v${VERSION}"
|
||||
echo " - sdk/agents/v${VERSION}"
|
||||
echo " - sdk/core/v${VERSION}"
|
||||
echo " - sdk/sdk/v${VERSION}"
|
||||
fi
|
||||
+35
-117
@@ -11,13 +11,8 @@ 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: "Tag to publish (required in both modes, e.g., v3.1.2)"
|
||||
description: "Enter existing tag to publish (e.g., v3.1.2)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
@@ -40,80 +35,37 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
ref: ${{ github.event.inputs.tag }}
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
lfs: true
|
||||
|
||||
- name: Resolve Release Tag
|
||||
id: resolve_tag
|
||||
env:
|
||||
TAG: ${{ github.event.inputs.tag }}
|
||||
AUTO_CREATE: ${{ github.event.inputs.auto_create_tag_from_main }}
|
||||
run: |
|
||||
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: 22
|
||||
node-version: "lts/*"
|
||||
|
||||
# Cache root dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache root dependencies
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache webview-ui dependencies
|
||||
uses: actions/cache@v4
|
||||
id: webview-cache
|
||||
with:
|
||||
path: webview-ui/node_modules
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm install --include=optional
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm install --include=optional
|
||||
|
||||
- name: Install Publishing Tools
|
||||
@@ -125,24 +77,20 @@ jobs:
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Verify Tag Matches Package Version
|
||||
- name: Validate Tag
|
||||
id: validate_tag
|
||||
run: |
|
||||
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'"
|
||||
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"
|
||||
exit 1
|
||||
fi
|
||||
echo "Tag and package version match: $TAG"
|
||||
|
||||
- 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
|
||||
echo "Tag '$TAG' validated successfully"
|
||||
|
||||
- name: Package and Publish Extension
|
||||
env:
|
||||
@@ -158,19 +106,11 @@ jobs:
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
RELEASE_TYPE: ${{ github.event.inputs.release-type }}
|
||||
run: |
|
||||
# Swap README.marketplace.md into README.md so both the GitHub
|
||||
# release artifact (vsce package below) and the marketplace
|
||||
# publish (npm run publish:marketplace below, which swaps
|
||||
# internally as an idempotent no-op) ship the same README.
|
||||
node scripts/marketplace-readme.mjs swap-in
|
||||
trap 'node scripts/marketplace-readme.mjs restore' EXIT
|
||||
|
||||
# Required to generate the .vsix
|
||||
vsce package --allow-package-secrets sendgrid --out "cline-${{ steps.get_version.outputs.version }}.vsix"
|
||||
|
||||
if [ "$RELEASE_TYPE" = "pre-release" ]; then
|
||||
if [ "${{ github.event.inputs.release-type }}" = "pre-release" ]; then
|
||||
npm run publish:marketplace:prerelease
|
||||
echo "Successfully published pre-release version ${{ steps.get_version.outputs.version }} to VS Code Marketplace and Open VSX Registry"
|
||||
else
|
||||
@@ -181,7 +121,7 @@ jobs:
|
||||
- name: Get Previous Tag
|
||||
id: prev_tag
|
||||
run: |
|
||||
CURRENT_TAG="${{ steps.resolve_tag.outputs.tag }}"
|
||||
CURRENT_TAG="${{ steps.validate_tag.outputs.tag }}"
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 "$CURRENT_TAG^" 2>/dev/null || echo "")
|
||||
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -197,34 +137,12 @@ jobs:
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ steps.resolve_tag.outputs.tag }}
|
||||
tag_name: ${{ steps.validate_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.resolve_tag.outputs.tag }}
|
||||
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.prev_tag.outputs.prev_tag }}...${{ steps.validate_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 }}"
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
name: SDK Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "sdk/**"
|
||||
- ".github/workflows/sdk-test.yml"
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "sdk/**"
|
||||
- ".github/workflows/sdk-test.yml"
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: sdk
|
||||
|
||||
jobs:
|
||||
quality-checks:
|
||||
runs-on: ubuntu-latest
|
||||
name: Quality Checks
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.3.13"
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Typecheck
|
||||
run: |
|
||||
bun run build:sdk
|
||||
bun run -F @cline/cli build
|
||||
bun run types
|
||||
|
||||
- name: Lint & Format
|
||||
run: bun run lint
|
||||
|
||||
test:
|
||||
needs: quality-checks
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
node-version: "24.x"
|
||||
- os: windows-latest
|
||||
node-version: "24.x"
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: Test (${{ matrix.os }}, Node ${{ matrix.node-version }})
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.3.13"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build SDK
|
||||
id: build_sdk_step
|
||||
run: bun run build:sdk
|
||||
|
||||
- name: Build CLI
|
||||
id: build_cli_step
|
||||
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' }}
|
||||
run: bun -F @cline/cli build
|
||||
|
||||
- name: Run Tests
|
||||
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os != 'windows-latest' }}
|
||||
run: bun run test
|
||||
|
||||
- name: Run SDK Tests (Windows)
|
||||
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os == 'windows-latest' }}
|
||||
run: bun -F './packages/**' test
|
||||
|
||||
- name: Smoke test SQLite under Node
|
||||
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && matrix.os != 'windows-latest' }}
|
||||
timeout-minutes: 10
|
||||
run: bun scripts/ci-node-smoke.ts
|
||||
|
||||
- name: Run TUI e2e tests
|
||||
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24.x' }}
|
||||
run: bun -F @cline/cli test:e2e:cli:tui
|
||||
|
||||
- name: Verify packages are publishable
|
||||
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24.x' }}
|
||||
run: bun scripts/check-publish.ts
|
||||
+66
-49
@@ -13,6 +13,8 @@ on:
|
||||
# Set default permissions for all jobs
|
||||
permissions:
|
||||
contents: read # Needed to check out code
|
||||
checks: write # Needed to report test results
|
||||
pull-requests: write # Needed to add comments/annotations to PRs
|
||||
|
||||
jobs:
|
||||
quality-checks:
|
||||
@@ -26,17 +28,27 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
webview-ui/package-lock.json
|
||||
|
||||
- name: Cache root dependencies
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Cache webview-ui dependencies
|
||||
uses: actions/cache@v4
|
||||
id: webview-cache
|
||||
with:
|
||||
path: webview-ui/node_modules
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Run Quality Checks (Parallel)
|
||||
@@ -44,8 +56,6 @@ jobs:
|
||||
|
||||
test:
|
||||
needs: quality-checks
|
||||
env:
|
||||
VSCODE_TEST_VERSION: 1.103.0
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -63,17 +73,27 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
webview-ui/package-lock.json
|
||||
|
||||
- name: Cache root dependencies
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Cache webview-ui dependencies
|
||||
uses: actions/cache@v4
|
||||
id: webview-cache
|
||||
with:
|
||||
path: webview-ui/node_modules
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Set up NPM on Windows
|
||||
@@ -81,13 +101,6 @@ jobs:
|
||||
run: |
|
||||
npm config set script-shell "C:\\Program Files\\Git\\bin\\bash.exe"
|
||||
|
||||
- name: Cache VS Code test runtime
|
||||
if: runner.os == 'Windows'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .vscode-test
|
||||
key: vscode-test-runtime-${{ runner.os }}-${{ env.VSCODE_TEST_VERSION }}
|
||||
|
||||
# Build the extension and tests (without redundant checks)
|
||||
- name: Build Tests and Extension
|
||||
id: build_step
|
||||
@@ -113,21 +126,7 @@ jobs:
|
||||
- name: Extension Integration Tests - Non-Linux
|
||||
id: integration_tests_non_linux
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
|
||||
run: |
|
||||
for attempt in 1 2 3; do
|
||||
echo "Running extension integration tests (attempt ${attempt}/3)"
|
||||
if npm run test:integration; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$attempt" -eq 3 ]; then
|
||||
echo "Extension integration tests failed after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Extension integration tests failed; retrying after short delay"
|
||||
sleep 5
|
||||
done
|
||||
run: npm run test:integration
|
||||
|
||||
- name: Webview Tests with Coverage
|
||||
id: webview_tests
|
||||
@@ -136,11 +135,6 @@ jobs:
|
||||
cd webview-ui
|
||||
npm run test:coverage
|
||||
|
||||
- name: CLI Tests
|
||||
id: cli_tests
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
|
||||
run: cd cli && npm run test:run
|
||||
|
||||
- name: Save Coverage Reports
|
||||
uses: actions/upload-artifact@v4
|
||||
# Only upload artifacts on Linux - We only need coverage from one OS
|
||||
@@ -162,18 +156,35 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
webview-ui/package-lock.json
|
||||
testing-platform/package-lock.json
|
||||
|
||||
- name: Cache root dependencies
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Cache webview-ui dependencies
|
||||
uses: actions/cache@v4
|
||||
id: webview-cache
|
||||
with:
|
||||
path: webview-ui/node_modules
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
|
||||
|
||||
# Cache testing-platform dependencies
|
||||
- name: Cache testing-platform dependencies
|
||||
uses: actions/cache@v4
|
||||
id: testing-platform-cache
|
||||
with:
|
||||
path: testing-platform/node_modules
|
||||
key: ${{ runner.os }}-npm-testing-platform-${{ hashFiles('testing-platform/package-lock.json') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Download ripgrep binaries
|
||||
@@ -183,11 +194,17 @@ jobs:
|
||||
run: npm run compile-standalone
|
||||
|
||||
- name: Install testing platform dependencies
|
||||
if: steps.testing-platform-cache.outputs.cache-hit != 'true'
|
||||
run: cd testing-platform && npm ci
|
||||
|
||||
- name: Running testing platform integration spec tests
|
||||
continue-on-error: true
|
||||
timeout-minutes: 7
|
||||
run: npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
|
||||
# Temporarily wrapping the test command to always return a neutral exit code.
|
||||
# This prevents the job from showing as failed and avoids distracting developers
|
||||
# until the integration tests are ready to be enforced.
|
||||
run: |
|
||||
npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
|
||||
|
||||
- name: Save Coverage Reports
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -20,8 +20,7 @@ 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(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.comment.author_association))
|
||||
contains(github.event.comment.body, '/test-jetbrains'))
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
id: app-token
|
||||
|
||||
-13
@@ -38,7 +38,6 @@ coverage-unit
|
||||
src/generated/
|
||||
src/shared/proto/
|
||||
webview-ui/src/services/grpc-client.ts
|
||||
*.tsbuildinfo
|
||||
|
||||
# E2E Tests
|
||||
test-results
|
||||
@@ -48,15 +47,3 @@ test-results
|
||||
.secrets
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
# Smoke test results (generated)
|
||||
evals/smoke-tests/results/
|
||||
|
||||
.tui-test
|
||||
secrets.json
|
||||
tui-traces
|
||||
tests/**/cache
|
||||
|
||||
# Backup created by scripts/marketplace-readme.mjs while publishing.
|
||||
# Should never be committed: only exists if a publish aborts mid-swap.
|
||||
.README.github.bak
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[submodule "evals/cline-bench"]
|
||||
path = evals/cline-bench
|
||||
url = https://github.com/cline/cline-bench.git
|
||||
+1
-2
@@ -3,8 +3,7 @@
|
||||
"ts"
|
||||
],
|
||||
"spec": [
|
||||
"src/**/__tests__/*.ts",
|
||||
"src/test/services/**/*.test.ts"
|
||||
"src/**/__tests__/*.ts"
|
||||
],
|
||||
"require": [
|
||||
"ts-node/register",
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
import { defineConfig } from "@vscode/test-cli"
|
||||
import path from "path"
|
||||
const vscodeTestVersion = process.env.VSCODE_TEST_VERSION ?? "stable"
|
||||
|
||||
export default defineConfig({
|
||||
files: "{out/**/*.test.js,src/**/*.test.js,!src/test/e2e/**/*.test.js,!out/src/test/e2e/**/*.test.js}",
|
||||
@@ -13,7 +12,7 @@ export default defineConfig({
|
||||
require: ["./test-setup.js"],
|
||||
},
|
||||
workspaceFolder: "test-workspace",
|
||||
version: vscodeTestVersion,
|
||||
version: "stable",
|
||||
extensionDevelopmentPath: path.resolve("./"),
|
||||
launchArgs: ["--disable-extensions"],
|
||||
})
|
||||
|
||||
Vendored
+1
-2
@@ -16,8 +16,7 @@
|
||||
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
|
||||
"--disable-extension",
|
||||
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
|
||||
"${workspaceFolder}",
|
||||
"--disable-extensions"
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/**/*.js"
|
||||
|
||||
+2
-9
@@ -25,15 +25,6 @@ eslint-rules/**
|
||||
# cli
|
||||
cli/**
|
||||
|
||||
# sdk (separate monorepo with its own build/release pipeline)
|
||||
sdk/**
|
||||
|
||||
# Source-of-truth for the marketplace README (the .vsix only ever sees the
|
||||
# README.md that scripts/marketplace-readme.mjs swaps into place). The backup
|
||||
# only exists if a publish aborts mid-swap; neither should ship in the .vsix.
|
||||
README.marketplace.md
|
||||
.README.github.bak
|
||||
|
||||
# Custom
|
||||
**/demo.gif
|
||||
.nvmrc
|
||||
@@ -44,9 +35,11 @@ README.marketplace.md
|
||||
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)
|
||||
|
||||
+2
-456
@@ -1,468 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## [3.82.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Restore VS Code foreground terminal support and settings.
|
||||
- Add latest OpenAI, SAP AI Core, and Z AI models.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix hook template JSON escaping.
|
||||
- Improve ripgrep file search error handling.
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove hardcoded model lists from docs.
|
||||
|
||||
## [3.81.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add GPT-5.5 model support for OpenAI Codex subscription users.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Remove hardcoded "What’s New" fallback items in webview; only remote-configured welcome banners are shown.
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve cline-core memory diagnostics used by the extension runtime:
|
||||
- enable near-heap-limit heap snapshots
|
||||
- add periodic memory usage logging
|
||||
- log discovered heap snapshots on abnormal exits for easier OOM debugging
|
||||
|
||||
## [3.80.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Wire up remote `globalSkills` from enterprise remote config with full UI, toggle support, and system prompt integration — enterprise-managed skills now appear under a dedicated "Enterprise Skills" section and support `alwaysEnabled` enforcement
|
||||
- Onboarding flow now uses dynamically fetched recommended models instead of a hardcoded list, with a fallback to the welcome view on failure
|
||||
- Add dedicated "Quota Exceeded" error message in the chat error UI when Cline account spend caps are hit
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix OOM crashes during long conversations by setting `--max-old-space-size=8192` for the cline-core Node.js process (was defaulting to ~2 GB)
|
||||
- Show detailed error information in the chat error row instead of a generic caught error message
|
||||
- Update `axios` to 1.15.0 across all packages
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove foreground terminal mode — all task command execution now defaults to background mode, removing the VS Code integrated terminal dependency and related settings UI
|
||||
- Remove old hardcoded announcement banners
|
||||
|
||||
## [3.79.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add Claude Opus 4.7 model support
|
||||
- Add Azure Blob Storage as a storage provider
|
||||
- Add `globalSkills` to remote config
|
||||
- Inline value reuse in user-level remote-config discovery
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix cache reflection for Cline and Vercel API handlers
|
||||
- Fix stuck `command_output` ask when terminal command ends unexpectedly
|
||||
- Add `use_subagents` to system prompt for GLM, Hermes, and XS models
|
||||
- Fix action injection security risk
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove deprecated evals tool
|
||||
|
||||
## [3.78.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add a dedicated "Spend Limit Reached" error UI when spend caps are hit
|
||||
- Docs updates
|
||||
|
||||
### Fixed
|
||||
|
||||
- Show actual `read_file` line ranges in chat UI
|
||||
|
||||
## [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
|
||||
|
||||
- Gemini-3.1 Pro Preview
|
||||
|
||||
|
||||
## [3.65.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add /skills slash command to CLI for viewing and managing installed skills
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix aggressive context compaction caused by accidental clicks on the context window progress bar silently setting a very low auto-condense threshold
|
||||
- Fix infinite retry loop when write_to_file fails with missing content parameter.
|
||||
- Fixed default claude model
|
||||
|
||||
## [3.64.0]
|
||||
|
||||
### Added
|
||||
- Added sonnet 4.6
|
||||
|
||||
|
||||
## [3.63.0]
|
||||
|
||||
### Added
|
||||
|
||||
- added zai GLM 5 Free promo
|
||||
|
||||
### Fixed
|
||||
|
||||
- Restore reasoning trace visibility in chat and improve the thinking row UX so reasoning is visible, then collapsible after completion.
|
||||
|
||||
## [3.62.0]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Banners now display immediately when opening the extension instead of requiring user interaction first
|
||||
- Resolved 17 security vulnerabilities including high-severity DoS issues in dependencies (body-parser, axios, qs, tar, and others)
|
||||
|
||||
## [3.61.0]
|
||||
|
||||
- UI/UX fixes with minimax model family
|
||||
|
||||
## [3.60.0]
|
||||
|
||||
- Fixes for Minimax model family
|
||||
|
||||
## [3.59.0]
|
||||
|
||||
- Added Minimax 2.5 Free Promo
|
||||
- Fixed Response chaining for OpenAI's Responses API
|
||||
|
||||
## [3.58.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Subagent: replace legacy subagents with the native `use_subagents` tool
|
||||
- Bundle `endpoints.json` support so packaged distributions can ship required endpoints out-of-the-box
|
||||
- Amazon Bedrock: support parallel tool calling
|
||||
- New "double-check completion" experimental feature to verify work before marking tasks complete
|
||||
- CLI: new task controls/flags including custom `--thinking` token budget and `--max-consecutive-mistakes` for yolo runs
|
||||
- Remote config: new UI/options (including connection/test buttons) and support for syncing deletion of remotely configured MCP servers
|
||||
- Vertex / Claude Code: add 1M context model options for Claude Opus 4.6
|
||||
- ZAI/GLM: add GLM-5
|
||||
|
||||
### Fixed
|
||||
|
||||
- CLI: handle stdin redirection correctly in CI/headless environments
|
||||
- CLI: preserve OAuth callback paths during auth redirects
|
||||
- VS Code Web: generate auth callback URLs via `vscode.env.asExternalUri` (OAuth callback reliability)
|
||||
- Terminal: surface command exit codes in results and improve long-running `execute_command` timeout behavior
|
||||
- UI: add loading indicator and fix `api_req_started` rendering
|
||||
- Task streaming: prevent duplicate streamed text rows after completion
|
||||
- API: preserve selected Vercel model when model metadata is missing
|
||||
- Telemetry: route PostHog networking through proxy-aware shared fetch and ensure telemetry flushes on shutdown
|
||||
- CI: increase Windows E2E test timeout to reduce flakiness
|
||||
|
||||
### Changed
|
||||
|
||||
- Settings/model UX: move "reasoning effort" into model configuration and expose it in settings
|
||||
- CLI provider selection: limit provider list to those remotely configured
|
||||
- UI: consolidate ViewHeader component/styling across views
|
||||
- Tools: add auto-approval support for `attempt_completion` commands
|
||||
- Remotely configured MCP server schema now supports custom headers
|
||||
|
||||
## [3.57.1]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Opus 4.6 for bedrock provider
|
||||
|
||||
## [3.57.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Cline CLI 2.0 now available. Install with `npm install -g cline`
|
||||
- Anthopic Opus 4.6
|
||||
- Minimax-2.1 and Kimi-k2.5 now available for free for a limited time promo
|
||||
- Codex-5.3 through ChatGPT subscription
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix read file tool to support reading large files
|
||||
- Fix decimal input crash in OpenAI Compatible price fields (#8129)
|
||||
- Fix build complete handlers when updating the api config
|
||||
- Fixed missing provider from list
|
||||
- Fixed Favorite Icon / Star from getting clipped in the task history view
|
||||
|
||||
### Changed
|
||||
|
||||
- Make skills always enabled and remove feature toggle setting
|
||||
|
||||
## [3.56.0]
|
||||
|
||||
### Added
|
||||
|
||||
- **CLI authentication:** Added Vercel AI Gateway and Cline API key provider support for headless CI/automation workflows
|
||||
- **New model:** Added Kimi-K2.5 model to Moonshot provider (262K context, image support, prompt caching)
|
||||
- **Prompt variant:** Added Trinity Large prompt variant for improved tool-calling support
|
||||
- **OpenTelemetry:** Added support for custom headers on metrics and logs endpoints
|
||||
- **Social links:** Added community icons (X, Discord, GitHub, Reddit, LinkedIn) to the What's New modal
|
||||
|
||||
### Fixed
|
||||
|
||||
- **LiteLLM:** Fixed thinking configuration not appearing for reasoning-capable models
|
||||
- **OpenTelemetry:** Fixed endpoint path handling (no longer incorrectly appends `/v1/logs` or `/v1/metrics`) and ensured logs are sent regardless of VSCode telemetry settings
|
||||
- **CLI auth:** Fixed `cline auth` displaying incorrect provider information after configuration
|
||||
|
||||
### Changed
|
||||
|
||||
- **Hooks:** Hook scripts now run from the workspace repository root instead of filesystem root
|
||||
- **Default settings:** Enabled multi-root workspaces, parallel tool calling, and skills by default; disabled strict plan mode by default
|
||||
- **Settings UI:** Refreshed feature settings section with collapsible design
|
||||
|
||||
## [3.55.0]
|
||||
|
||||
- Add new model: Arcee Trinity Large Preview
|
||||
- Add new model: Moonshot Kimi K2.5
|
||||
- Add MCP prompts support - prompts from connected MCP servers now appear in slash command autocomplete as `/mcp:<server>:<prompt>`
|
||||
|
||||
## [3.54.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Native tool calls support for Ollama provider
|
||||
- Sonnet 4.5 is now the default Amazon Bedrock model id
|
||||
|
||||
### Fixed
|
||||
|
||||
- Prevent infinite retry loops when replace_in_file fails repeatedly. The system now detects repeated failures and provides better guidance to break out of retry cycles.
|
||||
- Skip diff error UI handling during streaming to prevent flickering. Error handling is deferred until streaming completes.
|
||||
- Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing context size sent to the LLM.
|
||||
- Throttle diff view updates during streaming to reduce UI flickering and improve performance.
|
||||
|
||||
### Changed
|
||||
|
||||
- Removed Mistral's Devstral-2512 free from the free models list
|
||||
- Removed deprecated zai-glm-4.6 model from Cerebras provider
|
||||
|
||||
## [3.53.1]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bug in responses API
|
||||
- Bug in responses API
|
||||
|
||||
## [3.53.0]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Removed grok model from free tier
|
||||
- Removed grok model from free tier
|
||||
|
||||
## [3.52.0]
|
||||
|
||||
|
||||
+31
-11
@@ -42,25 +42,40 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
|
||||
```bash
|
||||
code cline
|
||||
```
|
||||
3. Install [bun](https://bun.com)
|
||||
4. Install the necessary dependencies for the extension and webview-gui:
|
||||
3. Install the necessary dependencies for the extension and webview-gui:
|
||||
```bash
|
||||
npm run install:all
|
||||
cd sdk && bun run build && cd ..
|
||||
```
|
||||
5. Generate Protocol Buffer files (required before first build):
|
||||
6. Launch by pressing `F5` (or `Run`->`Start Debugging`) to open a new VSCode window with the extension loaded. (You may need to install the [esbuild problem matchers extension](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) if you run into issues building the project.)
|
||||
4. Generate Protocol Buffer files (required before first build):
|
||||
```bash
|
||||
npm run protos
|
||||
```
|
||||
5. Launch by pressing `F5` (or `Run`->`Start Debugging`) to open a new VSCode window with the extension loaded. (You may need to install the [esbuild problem matchers extension](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) if you run into issues building the project.)
|
||||
|
||||
|
||||
|
||||
|
||||
### Creating a Pull Request
|
||||
|
||||
1. Commit your changes.
|
||||
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
|
||||
|
||||
2. Push your branch and create a PR on GitHub. Our CI will:
|
||||
2. Commit your changes and the generated `.changeset` file
|
||||
|
||||
3. Push your branch and create a PR on GitHub. Our CI will:
|
||||
- Run tests and checks
|
||||
3. Testing
|
||||
- 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
|
||||
- Run `npm run test` to run tests locally.
|
||||
- Before submitting PR, run `npm run format:fix` to format your code
|
||||
|
||||
@@ -177,10 +192,15 @@ 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. **Versioning & Changelog Notes**
|
||||
4. **Version Management with 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.
|
||||
- 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
|
||||
|
||||
5. **Commit Guidelines**
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2026 Cline Bot Inc.
|
||||
Copyright 2025 Cline Bot Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
<div align="center"><sub>
|
||||
English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">繁體中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/ko/README.md" target="_blank">한국어</a>
|
||||
</sub></div>
|
||||
|
||||
# Cline
|
||||
<div align="center">
|
||||
<table>
|
||||
<tbody>
|
||||
<td align="center">
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" target="_blank"><strong>Download on VS Marketplace</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://discord.gg/cline" target="_blank"><strong>Discord</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://www.reddit.com/r/cline/" target="_blank"><strong>r/cline</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://docs.cline.bot/getting-started/for-new-coders" target="_blank"><strong>Getting Started</strong></a>
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Meet Cline, an AI assistant that can use your **CLI** a**N**d **E**ditor.
|
||||
|
||||
Thanks to [Claude Sonnet's agentic coding capabilities](https://www.anthropic.com/claude/sonnet), Cline can handle complex software development tasks step-by-step. With tools that let him create & edit files, explore large projects, use the browser, and execute terminal commands (after you grant permission), he can assist you in ways that go beyond code completion or tech support. Cline can even use the Model Context Protocol (MCP) to create new tools and extend his own capabilities. While autonomous AI scripts traditionally run in sandboxed environments, this extension provides a human-in-the-loop GUI to approve every file change and terminal command, providing a safe and accessible way to explore the potential of agentic AI.
|
||||
|
||||
1. Enter your task and add images to convert mockups into functional apps or fix bugs with screenshots.
|
||||
2. Cline starts by analyzing your file structure & source code ASTs, running regex searches, and reading relevant files to get up to speed in existing projects. By carefully managing what information is added to context, Cline can provide valuable assistance even for large, complex projects without overwhelming the context window.
|
||||
3. Once Cline has the information he needs, he can:
|
||||
- Create and edit files + monitor linter/compiler errors along the way, letting him proactively fix issues like missing imports and syntax errors on his own.
|
||||
- Execute commands directly in your terminal and monitor their output as he works, letting him e.g., react to dev server issues after editing a file.
|
||||
- For web development tasks, Cline can launch the site in a headless browser, click, type, scroll, and capture screenshots + console logs, allowing him to fix runtime errors and visual bugs.
|
||||
4. When a task is completed, Cline will present the result to you with a terminal command like `open -a "Google Chrome" index.html`, which you run with a click of a button.
|
||||
|
||||
> [!TIP]
|
||||
> Follow [this guide](https://docs.cline.bot/features/customization/opening-cline-in-sidebar) to open Cline on the right side of your editor. This lets you use Cline side-by-side with your file explorer, and see how he changes your workspace more clearly.
|
||||
|
||||
---
|
||||
|
||||
<img align="right" width="340" src="https://github.com/user-attachments/assets/3cf21e04-7ce9-4d22-a7b9-ba2c595e88a4">
|
||||
|
||||
### Use any API and Model
|
||||
|
||||
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, Cerebras and Groq. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
|
||||
|
||||
The extension also keeps track of total tokens and API usage cost for the entire task loop and individual requests, keeping you informed of spend every step of the way.
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="left" width="370" src="https://github.com/user-attachments/assets/81be79a8-1fdb-4028-9129-5fe055e01e76">
|
||||
|
||||
### Run Commands in Terminal
|
||||
|
||||
Thanks to the new [shell integration updates in VSCode v1.93](https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api), Cline can execute commands directly in your terminal and receive the output. This allows him to perform a wide range of tasks, from installing packages and running build scripts to deploying applications, managing databases, and executing tests, all while adapting to your dev environment & toolchain to get the job done right.
|
||||
|
||||
For long running processes like dev servers, use the "Proceed While Running" button to let Cline continue in the task while the command runs in the background. As Cline works he’ll be notified of any new terminal output along the way, letting him react to issues that may come up, such as compile-time errors when editing files.
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="400" src="https://github.com/user-attachments/assets/c5977833-d9b8-491e-90f9-05f9cd38c588">
|
||||
|
||||
### Create and Edit Files
|
||||
|
||||
Cline can create and edit files directly in your editor, presenting you a diff view of the changes. You can edit or revert Cline's changes directly in the diff view editor, or provide feedback in chat until you're satisfied with the result. Cline also monitors linter/compiler errors (missing imports, syntax errors, etc.) so he can fix issues that come up along the way on his own.
|
||||
|
||||
All changes made by Cline are recorded in your file's Timeline, providing an easy way to track and revert modifications if needed.
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="left" width="370" src="https://github.com/user-attachments/assets/bc2e85ba-dfeb-4fe6-9942-7cfc4703cbe5">
|
||||
|
||||
### Use the Browser
|
||||
|
||||
With Claude Sonnet's new [Computer Use](https://www.anthropic.com/news/3-5-models-and-computer-use) capability, Cline can launch a browser, click elements, type text, and scroll, capturing screenshots and console logs at each step. This allows for interactive debugging, end-to-end testing, and even general web use! This gives him autonomy to fixing visual bugs and runtime issues without you needing to handhold and copy-pasting error logs yourself.
|
||||
|
||||
Try asking Cline to "test the app", and watch as he runs a command like `npm run dev`, launches your locally running dev server in a browser, and performs a series of tests to confirm that everything works. [See a demo here.](https://x.com/sdrzn/status/1850880547825823989)
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="350" src="https://github.com/user-attachments/assets/ac0efa14-5c1f-4c26-a42d-9d7c56f5fadd">
|
||||
|
||||
### "add a tool that..."
|
||||
|
||||
Thanks to the [Model Context Protocol](https://github.com/modelcontextprotocol), Cline can extend his capabilities through custom tools. While you can use [community-made servers](https://github.com/modelcontextprotocol/servers), Cline can instead create and install tools tailored to your specific workflow. Just ask Cline to "add a tool" and he will handle everything, from creating a new MCP server to installing it into the extension. These custom tools then become part of Cline's toolkit, ready to use in future tasks.
|
||||
|
||||
- "add a tool that fetches Jira tickets": Retrieve ticket ACs and put Cline to work
|
||||
- "add a tool that manages AWS EC2s": Check server metrics and scale instances up or down
|
||||
- "add a tool that pulls the latest PagerDuty incidents": Fetch details and ask Cline to fix bugs
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="left" width="360" src="https://github.com/user-attachments/assets/7fdf41e6-281a-4b4b-ac19-020b838b6970">
|
||||
|
||||
### Add Context
|
||||
|
||||
**`@url`:** Paste in a URL for the extension to fetch and convert to markdown, useful when you want to give Cline the latest docs
|
||||
|
||||
**`@problems`:** Add workspace errors and warnings ('Problems' panel) for Cline to fix
|
||||
|
||||
**`@file`:** Adds a file's contents so you don't have to waste API requests approving read file (+ type to search files)
|
||||
|
||||
**`@folder`:** Adds folder's files all at once to speed up your workflow even more
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="350" src="https://github.com/user-attachments/assets/140c8606-d3bf-41b9-9a1f-4dbf0d4c90cb">
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
## Contributing
|
||||
|
||||
To contribute to the project, start with our [Contributing Guide](CONTRIBUTING.md) to learn the basics. You can also join our [Discord](https://discord.gg/cline) to chat with other contributors in the `#contributors` channel. If you're looking for full-time work, check out our open positions on our [careers page](https://cline.bot/join-us)!
|
||||
|
||||
## Enterprise
|
||||
|
||||
Get the same Cline experience with enterprise-grade controls: SSO (SAML/OIDC), global policies and configuration, observability with audit trails, private networking (VPC/private link), and self-hosted or on-prem deployments, and enterprise support. Learn more at our [enterprise page](https://cline.bot/enterprise) or [talk to us](https://cline.bot/contact-sales).
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0 © 2026 Cline Bot Inc.](./LICENSE)
|
||||
@@ -1,20 +1,18 @@
|
||||
<p align="center">
|
||||
<img src="assets/icons/icon.png" width="80" alt="Cline" />
|
||||
</p>
|
||||
<div align="center"><sub>
|
||||
English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">繁體中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/ko/README.md" target="_blank">한국어</a>
|
||||
</sub></div>
|
||||
|
||||
<h1 align="center">Cline</h1>
|
||||
# Cline
|
||||
|
||||
<p align="center">
|
||||
The open source coding agent in your IDE and terminal.
|
||||
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<div align="center">
|
||||
<table>
|
||||
<tbody>
|
||||
<td align="center">
|
||||
<a href="https://docs.cline.bot" target="_blank"><strong>Docs</strong></a>
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" target="_blank"><strong>Download on VS Marketplace</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://discord.gg/cline" target="_blank"><strong>Discord</strong></a>
|
||||
@@ -26,210 +24,128 @@ The open source coding agent in your IDE and terminal.
|
||||
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://cline.bot/join-us" target="_blank"><strong>Join us!</strong></a>
|
||||
<a href="https://docs.cline.bot/getting-started/for-new-coders" target="_blank"><strong>Getting Started</strong></a>
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
Meet Cline, an AI assistant that can use your **CLI** a**N**d **E**ditor.
|
||||
|
||||
<br>
|
||||
Thanks to [Claude Sonnet's agentic coding capabilities](https://www.anthropic.com/claude/sonnet), Cline can handle complex software development tasks step-by-step. With tools that let him create & edit files, explore large projects, use the browser, and execute terminal commands (after you grant permission), he can assist you in ways that go beyond code completion or tech support. Cline can even use the Model Context Protocol (MCP) to create new tools and extend his own capabilities. While autonomous AI scripts traditionally run in sandboxed environments, this extension provides a human-in-the-loop GUI to approve every file change and terminal command, providing a safe and accessible way to explore the potential of agentic AI.
|
||||
|
||||
<div align="center">
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" width="50%">
|
||||
1. Enter your task and add images to convert mockups into functional apps or fix bugs with screenshots.
|
||||
2. Cline starts by analyzing your file structure & source code ASTs, running regex searches, and reading relevant files to get up to speed in existing projects. By carefully managing what information is added to context, Cline can provide valuable assistance even for large, complex projects without overwhelming the context window.
|
||||
3. Once Cline has the information he needs, he can:
|
||||
- Create and edit files + monitor linter/compiler errors along the way, letting him proactively fix issues like missing imports and syntax errors on his own.
|
||||
- Execute commands directly in your terminal and monitor their output as he works, letting him e.g., react to dev server issues after editing a file.
|
||||
- For web development tasks, Cline can launch the site in a headless browser, click, type, scroll, and capture screenshots + console logs, allowing him to fix runtime errors and visual bugs.
|
||||
4. When a task is completed, Cline will present the result to you with a terminal command like `open -a "Google Chrome" index.html`, which you run with a click of a button.
|
||||
|
||||
### CLI
|
||||
|
||||
Run Cline in your terminal.
|
||||
Interactive chat or fully headless
|
||||
for CI/CD and scripting.
|
||||
|
||||
```
|
||||
npm i -g cline
|
||||
```
|
||||
|
||||
<a href="./sdk/apps/cli/README.md">Learn more</a>
|
||||
<br><br>
|
||||
|
||||
</td>
|
||||
<td align="center" width="50%">
|
||||
|
||||
### Kanban
|
||||
|
||||
Run many agents in parallel from a
|
||||
web-based task board. Each card gets its own
|
||||
worktree, auto-commit, and dependency chains.
|
||||
|
||||
```
|
||||
npm i -g kanban
|
||||
```
|
||||
|
||||
<a href="https://github.com/cline/kanban">Learn more</a>
|
||||
<br><br>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="50%">
|
||||
|
||||
### VS Code Extension
|
||||
|
||||
AI coding assistant in your editor.
|
||||
Create files, run commands, browse the web,
|
||||
and use tools with human-in-the-loop approval.
|
||||
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev">Install from VS Marketplace</a>
|
||||
<br><br>
|
||||
|
||||
</td>
|
||||
<td align="center" width="50%">
|
||||
|
||||
### JetBrains Plugin
|
||||
|
||||
The same Cline experience in IntelliJ IDEA,
|
||||
PyCharm, WebStorm, GoLand, and the rest of
|
||||
the JetBrains family.
|
||||
|
||||
<a href="https://plugins.jetbrains.com/plugin/28247-cline">Install from JetBrains Marketplace</a>
|
||||
<br><br>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
|
||||
### SDK
|
||||
|
||||
Build your own AI agents and integrations powered by the same engine that runs the CLI, Kanban, VS Code extension, and JetBrains plugin. Custom tools, multi-agent teams, connectors, scheduled automations, and more.
|
||||
|
||||
```
|
||||
npm install @cline/sdk
|
||||
```
|
||||
|
||||
<a href="https://docs.cline.bot/cline-sdk/overview">Documentation</a>
|
||||
<br><br>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
> [!TIP]
|
||||
> Follow [this guide](https://docs.cline.bot/features/customization/opening-cline-in-sidebar) to open Cline on the right side of your editor. This lets you use Cline side-by-side with your file explorer, and see how he changes your workspace more clearly.
|
||||
|
||||
---
|
||||
|
||||
## Index
|
||||
<img align="right" width="340" src="https://github.com/user-attachments/assets/3cf21e04-7ce9-4d22-a7b9-ba2c595e88a4">
|
||||
|
||||
| Product | Description | Location |
|
||||
|---------|------------|--------------|
|
||||
| **SDK** | Node.js programmatic agent API and extension exports. | [`sdk/`](https://github.com/cline/cline/tree/main/sdk) |
|
||||
| **CLI** | Terminal UI, headless mode, shell commands, and CLI-specific flows. | [`sdk/apps/cli/`](https://github.com/cline/cline/tree/main/sdk/apps/cli) |
|
||||
| **VS Code Extension** | The Marketplace extension and extension host integration. | [`/`](https://github.com/cline/cline/tree/main) (WIP migrating) |
|
||||
| **JetBrains Plugin** | JetBrains-hosted client that talks to the shared agent core. | Currently we are not open-sourcing JetBrains plugins |
|
||||
| **Kanban** | Web-based multi-agent task board. | [`cline/kanban`](https://github.com/cline/kanban). |
|
||||
| **Docs site** | Public documentation pages. | [`docs/`](https://docs.cline.bot/) |
|
||||
### Use any API and Model
|
||||
|
||||
## Edits Code Across Your Project
|
||||
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, Cerebras and Groq. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
|
||||
|
||||
Cline reads your project structure, understands the relationships between files, and makes coordinated changes across your codebase. It monitors linter and compiler errors as it works, fixing issues like missing imports, type mismatches, and syntax errors before you even see them. In VS Code and JetBrains, every edit shows up as a diff you can review, modify, or revert. All changes are tracked with checkpoints, so you can easily undo the agent's work.
|
||||
The extension also keeps track of total tokens and API usage cost for the entire task loop and individual requests, keeping you informed of spend every step of the way.
|
||||
|
||||
## Runs Bash Commands
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
Cline executes commands directly in your terminal and watches the output in real time. Install packages, run build scripts, execute tests, deploy applications, manage databases. For long-running processes like dev servers, Cline continues working in the background and reacts to new output as it appears, catching compile errors, test failures, and server crashes as they happen.
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
## Plan and Act
|
||||
<img align="left" width="370" src="https://github.com/user-attachments/assets/81be79a8-1fdb-4028-9129-5fe055e01e76">
|
||||
|
||||
Toggle between Plan mode and Act mode. In Plan mode, Cline explores your codebase, asks clarifying questions, and lays out a strategy. Once you're aligned, switch to Act mode and Cline executes the plan. Every file edit and terminal command requires your approval, so you stay in control of what actually changes. Or toggle auto-approve and let Cline run autonomously.
|
||||
### Run Commands in Terminal
|
||||
|
||||
## Rules and Skills
|
||||
Thanks to the new [shell integration updates in VSCode v1.93](https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api), Cline can execute commands directly in your terminal and receive the output. This allows him to perform a wide range of tasks, from installing packages and running build scripts to deploying applications, managing databases, and executing tests, all while adapting to your dev environment & toolchain to get the job done right.
|
||||
|
||||
Define project-specific rules in `.clinerules` files that guide how Cline works in your codebase: coding standards, architecture conventions, deployment procedures, testing requirements. Rules are picked up automatically by the CLI, VS Code extension, and JetBrains plugin. Use skills to let the model load specific rules when needed.
|
||||
For long running processes like dev servers, use the "Proceed While Running" button to let Cline continue in the task while the command runs in the background. As Cline works he’ll be notified of any new terminal output along the way, letting him react to issues that may come up, such as compile-time errors when editing files.
|
||||
|
||||
## Works With Every Model
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
Cline is not locked to a single AI provider. Use whichever model fits your workflow:
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
| Provider | Models |
|
||||
|----------|--------|
|
||||
| Anthropic | Claude Opus, Sonnet, Haiku |
|
||||
| OpenAI | GPT series model |
|
||||
| Google | Gemini series model |
|
||||
| OpenRouter | 200+ models from any provider |
|
||||
| Vercel AI Gateway | Models through Vercel AI Gateway |
|
||||
| AWS Bedrock | Claude, Llama, and more |
|
||||
| Azure / GCP Vertex | All hosted models |
|
||||
| Cerebras / Groq | Fast inference models |
|
||||
| Ollama / LM Studio | Run local models on your machine |
|
||||
| Any OpenAI-compatible API | Self-hosted or third-party endpoints |
|
||||
<img align="right" width="400" src="https://github.com/user-attachments/assets/c5977833-d9b8-491e-90f9-05f9cd38c588">
|
||||
|
||||
## Extend With Plugins or MCP Servers
|
||||
### Create and Edit Files
|
||||
|
||||
Extend Cline's capabilities with plugins. Using the SDK, register tools and lifecycle hooks programmatically through the plugin system for logging, auditing, policy enforcement, or adding domain-specific capabilities. Simple plugin example below.
|
||||
Cline can create and edit files directly in your editor, presenting you a diff view of the changes. You can edit or revert Cline's changes directly in the diff view editor, or provide feedback in chat until you're satisfied with the result. Cline also monitors linter/compiler errors (missing imports, syntax errors, etc.) so he can fix issues that come up along the way on his own.
|
||||
|
||||
```typescript
|
||||
import { Agent, createTool } from "@cline/sdk"
|
||||
All changes made by Cline are recorded in your file's Timeline, providing an easy way to track and revert modifications if needed.
|
||||
|
||||
const deployTool = createTool({
|
||||
name: "deploy",
|
||||
description: "Deploy the current branch to staging.",
|
||||
inputSchema: { type: "object", properties: { env: { type: "string" } }, required: ["env"] },
|
||||
execute: async (input) => {
|
||||
// your deployment logic
|
||||
},
|
||||
})
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
const agent = new Agent({ tools: [deployTool], /* ... */ })
|
||||
```
|
||||
...or use [MCP servers](https://github.com/modelcontextprotocol) to connect to databases, query APIs, manage cloud infrastructure, and interact with external systems. Use [community-built servers](https://github.com/modelcontextprotocol/servers) or ask Cline to create custom tools on the fly. In the CLI, manage servers with `cline mcp`.
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
## Multi-Agent Teams
|
||||
<img align="left" width="370" src="https://github.com/user-attachments/assets/bc2e85ba-dfeb-4fe6-9942-7cfc4703cbe5">
|
||||
|
||||
Coordinate multiple agents working together on complex tasks. A coordinator agent breaks the work into subtasks and delegates to specialist agents, each with their own tools and context. Team state persists across sessions so you can pick up where you left off.
|
||||
### Use the Browser
|
||||
|
||||
```bash
|
||||
cline --team-name auth-sprint "Plan and implement user authentication with tests"
|
||||
```
|
||||
With Claude Sonnet's new [Computer Use](https://www.anthropic.com/news/3-5-models-and-computer-use) capability, Cline can launch a browser, click elements, type text, and scroll, capturing screenshots and console logs at each step. This allows for interactive debugging, end-to-end testing, and even general web use! This gives him autonomy to fixing visual bugs and runtime issues without you needing to handhold and copy-pasting error logs yourself.
|
||||
|
||||
## Scheduled Agents
|
||||
Try asking Cline to "test the app", and watch as he runs a command like `npm run dev`, launches your locally running dev server in a browser, and performs a series of tests to confirm that everything works. [See a demo here.](https://x.com/sdrzn/status/1850880547825823989)
|
||||
|
||||
Run agents on cron schedules for recurring automations. Daily PR summaries, weekly dependency checks, codebase health reports. Schedules persist across restarts and run independently of any terminal session.
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
```bash
|
||||
cline schedule create "PR summary" \
|
||||
--cron "0 9 * * MON-FRI" \
|
||||
--prompt "List all open PRs and their review status" \
|
||||
--workspace /path/to/repo
|
||||
```
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
## Connect to Slack, Telegram, Discord, and More
|
||||
<img align="right" width="350" src="https://github.com/user-attachments/assets/ac0efa14-5c1f-4c26-a42d-9d7c56f5fadd">
|
||||
|
||||
Chat with your agent from any messaging platform: Telegram, Slack, Discord, Google Chat, WhatsApp, and Linear. Each conversation thread maps to an agent session with full context. Set up access control to restrict who can interact with your agent.
|
||||
### "add a tool that..."
|
||||
|
||||
```bash
|
||||
cline connect telegram -m my_bot -k $BOT_TOKEN
|
||||
cline connect slack --token $SLACK_TOKEN --signing-secret $SECRET --base-url $URL
|
||||
```
|
||||
Thanks to the [Model Context Protocol](https://github.com/modelcontextprotocol), Cline can extend his capabilities through custom tools. While you can use [community-made servers](https://github.com/modelcontextprotocol/servers), Cline can instead create and install tools tailored to your specific workflow. Just ask Cline to "add a tool" and he will handle everything, from creating a new MCP server to installing it into the extension. These custom tools then become part of Cline's toolkit, ready to use in future tasks.
|
||||
|
||||
## Headless CLI for CI/CD
|
||||
- "add a tool that fetches Jira tickets": Retrieve ticket ACs and put Cline to work
|
||||
- "add a tool that manages AWS EC2s": Check server metrics and scale instances up or down
|
||||
- "add a tool that pulls the latest PagerDuty incidents": Fetch details and ask Cline to fix bugs
|
||||
|
||||
Run Cline with zero interaction for scripting and automation. Pipe input, get JSON output, chain commands, integrate into CI/CD pipelines.
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
```bash
|
||||
cline "Run tests and fix any failures"
|
||||
git diff origin/main | cline "Review these changes for issues"
|
||||
cline --json "List all TODO comments" | jq -r 'select(.type == "agent_event" and .event.text) | .event.text'
|
||||
```
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="left" width="360" src="https://github.com/user-attachments/assets/7fdf41e6-281a-4b4b-ac19-020b838b6970">
|
||||
|
||||
### Add Context
|
||||
|
||||
**`@url`:** Paste in a URL for the extension to fetch and convert to markdown, useful when you want to give Cline the latest docs
|
||||
|
||||
**`@problems`:** Add workspace errors and warnings ('Problems' panel) for Cline to fix
|
||||
|
||||
**`@file`:** Adds a file's contents so you don't have to waste API requests approving read file (+ type to search files)
|
||||
|
||||
**`@folder`:** Adds folder's files all at once to speed up your workflow even more
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="350" src="https://github.com/user-attachments/assets/140c8606-d3bf-41b9-9a1f-4dbf0d4c90cb">
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
## Contributing
|
||||
|
||||
Start with the [Contributing Guide](CONTRIBUTING.md). Join our [Discord](https://discord.gg/cline) and head to the `#contributors` channel to connect with other contributors. Check our [careers page](https://cline.bot/join-us) for full-time roles.
|
||||
To contribute to the project, start with our [Contributing Guide](CONTRIBUTING.md) to learn the basics. You can also join our [Discord](https://discord.gg/cline) to chat with other contributors in the `#contributors` channel. If you're looking for full-time work, check out our open positions on our [careers page](https://cline.bot/join-us)!
|
||||
|
||||
## Enterprise
|
||||
|
||||
Get the same Cline experience with enterprise-grade controls: SSO (SAML/OIDC), global policies and configuration, observability with audit trails, private networking (VPC/private link), and self-hosted or on-prem deployments, and enterprise support. Learn more at our [enterprise page](https://cline.bot/enterprise) or [talk to us](https://cline.bot/contact-sales).
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0 © 2026 Cline Bot Inc.](./LICENSE)
|
||||
[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE)
|
||||
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We actively patch only the most recent minor release of Cline. Older versions receive fixes at our discretion.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We appreciate your efforts to responsibly disclose your findings and will make every effort to acknowledge your contributions.
|
||||
|
||||
To report a security issue, please submit your report through our [Bugcrowd Vulnerability Disclosure Program](https://bugcrowd.com/engagements/clinebot-vdp-ess). Bugcrowd will manage communication and triage on our behalf.
|
||||
|
||||
When reporting, please include:
|
||||
|
||||
- A short summary of the issue
|
||||
- Steps to reproduce or a proof of concept
|
||||
- Any logs, stack traces, or screenshots that might help us understand the problem
|
||||
|
||||
Please keep the details private until a resolution has been reached.
|
||||
|
||||
## Escalation
|
||||
|
||||
If you are unable to submit through Bugcrowd, you may send an email to security@cline.bot.
|
||||
|
||||
Thank you for helping us keep Cline users safe.
|
||||
@@ -1,12 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.317 4.15557C18.7873 3.45369 17.147 2.93658 15.4319 2.6404C15.4007 2.63469 15.3695 2.64897 15.3534 2.67754C15.1424 3.05276 14.9087 3.54225 14.7451 3.927C12.9004 3.65083 11.0652 3.65083 9.25832 3.927C9.09465 3.5337 8.85248 3.05276 8.64057 2.67754C8.62449 2.64992 8.59328 2.63564 8.56205 2.6404C6.84791 2.93563 5.20756 3.45275 3.67693 4.15557C3.66368 4.16129 3.65233 4.17082 3.64479 4.18319C0.533392 8.83155 -0.31895 13.3657 0.0991801 17.8436C0.101072 17.8655 0.11337 17.8864 0.130398 17.8997C2.18321 19.4073 4.17171 20.3225 6.12328 20.9291C6.15451 20.9386 6.18761 20.9272 6.20748 20.9015C6.66913 20.2711 7.08064 19.6063 7.43348 18.9073C7.4543 18.8664 7.43442 18.8178 7.39186 18.8016C6.73913 18.554 6.1176 18.2521 5.51973 17.9093C5.47244 17.8816 5.46865 17.814 5.51216 17.7816C5.63797 17.6873 5.76382 17.5893 5.88396 17.4902C5.90569 17.4721 5.93598 17.4683 5.96153 17.4797C9.88928 19.273 14.1415 19.273 18.023 17.4797C18.0485 17.4674 18.0788 17.4712 18.1015 17.4893C18.2216 17.5883 18.3475 17.6873 18.4742 17.7816C18.5177 17.814 18.5149 17.8816 18.4676 17.9093C17.8697 18.2588 17.2482 18.554 16.5945 18.8006C16.552 18.8168 16.533 18.8664 16.5538 18.9073C16.9143 19.6054 17.3258 20.2701 17.7789 20.9005C17.7978 20.9272 17.8319 20.9386 17.8631 20.9291C19.8241 20.3225 21.8126 19.4073 23.8654 17.8997C23.8834 17.8864 23.8948 17.8664 23.8967 17.8445C24.3971 12.6676 23.0585 8.17064 20.3482 4.18414C20.3416 4.17082 20.3303 4.16129 20.317 4.15557ZM8.02002 15.117C6.8375 15.117 5.86313 14.0313 5.86313 12.6981C5.86313 11.3648 6.8186 10.2791 8.02002 10.2791C9.23087 10.2791 10.1958 11.3743 10.1769 12.6981C10.1769 14.0313 9.22141 15.117 8.02002 15.117ZM15.9947 15.117C14.8123 15.117 13.8379 14.0313 13.8379 12.6981C13.8379 11.3648 14.7933 10.2791 15.9947 10.2791C17.2056 10.2791 18.1705 11.3743 18.1516 12.6981C18.1516 14.0313 17.2056 15.117 15.9947 15.117Z" fill="#FAFAFA"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="black" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2C6.477 2 2 6.477 2 12C2 16.418 4.865 20.166 8.84 21.49C9.34 21.58 9.52 21.27 9.52 21C9.52 20.77 9.51 20.14 9.51 19.31C6.73 19.91 6.14 17.97 6.14 17.97C5.68 16.81 5.03 16.5 5.03 16.5C4.12 15.88 5.1 15.9 5.1 15.9C6.1 15.97 6.63 16.93 6.63 16.93C7.5 18.45 8.97 18 9.54 17.76C9.63 17.11 9.89 16.67 10.17 16.42C7.95 16.17 5.62 15.31 5.62 11.5C5.62 10.39 6 9.5 6.65 8.79C6.55 8.54 6.2 7.5 6.75 6.15C6.75 6.15 7.59 5.88 9.5 7.17C10.29 6.95 11.15 6.84 12 6.84C12.85 6.84 13.71 6.95 14.5 7.17C16.41 5.88 17.25 6.15 17.25 6.15C17.8 7.5 17.45 8.54 17.35 8.79C18 9.5 18.38 10.39 18.38 11.5C18.38 15.32 16.04 16.16 13.81 16.41C14.17 16.72 14.5 17.33 14.5 18.26C14.5 19.6 14.49 20.68 14.49 21C14.49 21.27 14.67 21.59 15.17 21.49C19.14 20.16 22 16.42 22 12C22 6.477 17.523 2 12 2Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 902 B |
@@ -1,10 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2001_1428)">
|
||||
<path d="M22.2234 0H1.77187C0.792187 0 0 0.773438 0 1.72969V22.2656C0 23.2219 0.792187 24 1.77187 24H22.2234C23.2031 24 24 23.2219 24 22.2703V1.72969C24 0.773438 23.2031 0 22.2234 0ZM7.12031 20.4516H3.55781V8.99531H7.12031V20.4516ZM5.33906 7.43438C4.19531 7.43438 3.27188 6.51094 3.27188 5.37187C3.27188 4.23281 4.19531 3.30937 5.33906 3.30937C6.47813 3.30937 7.40156 4.23281 7.40156 5.37187C7.40156 6.50625 6.47813 7.43438 5.33906 7.43438ZM20.4516 20.4516H16.8937V14.8828C16.8937 13.5562 16.8703 11.8453 15.0422 11.8453C13.1906 11.8453 12.9094 13.2937 12.9094 14.7891V20.4516H9.35625V8.99531H12.7687V10.5609H12.8156C13.2891 9.66094 14.4516 8.70938 16.1813 8.70938C19.7859 8.70938 20.4516 11.0813 20.4516 14.1656V20.4516Z" fill="#FAFAFA"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2001_1428">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 989 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.0512 4.07466C15.3113 5.17727 16.301 5.99866 17.4829 5.99866C18.8627 5.99866 19.9813 4.87965 19.9813 3.49933C19.9813 2.11902 18.8627 1 17.4829 1C16.2764 1 15.2703 1.85537 15.036 2.99314C13.0155 3.20991 11.4378 4.92417 11.4378 7.00167C11.4378 7.00636 11.4378 7.00988 11.4378 7.01456C9.24041 7.10713 7.23397 7.73284 5.641 8.72062C5.04949 8.26247 4.30688 7.98945 3.50102 7.98945C1.5672 7.98945 0 9.55725 0 11.4918C0 12.8955 0.824597 14.1048 2.01581 14.6637C2.13177 18.7297 6.56047 22 12.0082 22C17.4559 22 21.8905 18.7261 22.0006 14.6567C23.1824 14.0942 24 12.8885 24 11.493C24 9.55842 22.4328 7.99063 20.499 7.99063C19.6966 7.99063 18.9575 8.2613 18.3672 8.71594C16.7602 7.72113 14.7315 7.09541 12.5119 7.01222C12.5119 7.0087 12.5119 7.00636 12.5119 7.00285C12.5119 5.51473 13.6176 4.27971 15.0512 4.077V4.07466ZM5.50044 13.7146C5.559 12.4444 6.40234 11.4695 7.38272 11.4695C8.3631 11.4695 9.11274 12.4995 9.05417 13.7697C8.99561 15.0398 8.26354 15.5015 7.28199 15.5015C6.30044 15.5015 5.44187 14.9848 5.50044 13.7146ZM16.6348 11.4695C17.6164 11.4695 18.4597 12.4444 18.5171 13.7146C18.5757 14.9848 17.716 15.5015 16.7356 15.5015C15.7552 15.5015 15.022 15.041 14.9634 13.7697C14.9048 12.4995 15.6533 11.4695 16.6348 11.4695ZM15.4682 16.6533C15.6521 16.6721 15.7693 16.8631 15.6978 17.0341C15.0946 18.4766 13.6703 19.4901 12.0082 19.4901C10.3461 19.4901 8.92299 18.4766 8.31859 17.0341C8.24714 16.8631 8.36427 16.6721 8.54817 16.6533C9.62577 16.5444 10.7912 16.4846 12.0082 16.4846C13.2252 16.4846 14.3895 16.5444 15.4682 16.6533Z" fill="#FAFAFA"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.3263 1.90393H21.6998L14.3297 10.3274L23 21.7899H16.2112L10.894 14.838L4.80995 21.7899H1.43443L9.31743 12.78L1 1.90393H7.96111L12.7674 8.25826L18.3263 1.90393ZM17.1423 19.7707H19.0116L6.94539 3.81706H4.93946L17.1423 19.7707Z" fill="#FAFAFA"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 358 B |
+66
-77
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.1.4/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
@@ -28,19 +28,19 @@
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"correctness": {
|
||||
"useExhaustiveDependencies": "info",
|
||||
"useExhaustiveDependencies": "off",
|
||||
"noUndeclaredVariables": "off",
|
||||
"noEmptyPattern": "info",
|
||||
"noEmptyPattern": "off",
|
||||
"useJsxKeyInIterable": "off",
|
||||
"noInnerDeclarations": "off",
|
||||
"useHookAtTopLevel": "info",
|
||||
"useYield": "info",
|
||||
"useHookAtTopLevel": "off",
|
||||
"useYield": "off",
|
||||
"noConstructorReturn": "off",
|
||||
"noInvalidPositionAtImportRule": "off",
|
||||
"noSwitchDeclarations": "off",
|
||||
"noUnusedImports": "error"
|
||||
},
|
||||
"a11y": "info",
|
||||
"a11y": "off",
|
||||
"style": {
|
||||
"useNodejsImportProtocol": "off",
|
||||
"useImportType": "off",
|
||||
@@ -51,36 +51,35 @@
|
||||
"noParameterAssign": "off",
|
||||
"useAsConstAssertion": "off",
|
||||
"useDefaultParameterLast": "off",
|
||||
"noNonNullAssertion": "info",
|
||||
"noNonNullAssertion": "off",
|
||||
"useEnumInitializers": "off",
|
||||
"useSelfClosingElements": "info",
|
||||
"useSelfClosingElements": "off",
|
||||
"useSingleVarDeclarator": "off",
|
||||
"useNumberNamespace": "info",
|
||||
"noInferrableTypes": "info",
|
||||
"useTemplate": "info",
|
||||
"noUselessElse": "info"
|
||||
"useNumberNamespace": "off",
|
||||
"noInferrableTypes": "off",
|
||||
"useTemplate": "off",
|
||||
"noUselessElse": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noDoubleEquals": "warn",
|
||||
"noImplicitAnyLet": "info",
|
||||
"noThenProperty": "off",
|
||||
"noAsyncPromiseExecutor": "info",
|
||||
"noAsyncPromiseExecutor": "off",
|
||||
"noImportAssign": "off",
|
||||
"noExplicitAny": "info",
|
||||
"noControlCharactersInRegex": "warn",
|
||||
"noExplicitAny": "off",
|
||||
"noControlCharactersInRegex": "off",
|
||||
"noShadowRestrictedNames": "off",
|
||||
"noArrayIndexKey": "info",
|
||||
"noAssignInExpressions": "info",
|
||||
"useIterableCallbackReturn": "info"
|
||||
"noAssignInExpressions": "info"
|
||||
},
|
||||
"complexity": {
|
||||
"noUselessConstructor": "info",
|
||||
"useOptionalChain": "info",
|
||||
"noBannedTypes": "warn",
|
||||
"useLiteralKeys": "info",
|
||||
"noUselessCatch": "info",
|
||||
"noUselessSwitchCase": "info",
|
||||
"noStaticOnlyClass": "info"
|
||||
"noUselessConstructor": "off",
|
||||
"useOptionalChain": "off",
|
||||
"noBannedTypes": "off",
|
||||
"useLiteralKeys": "off",
|
||||
"noUselessCatch": "off",
|
||||
"noUselessSwitchCase": "off",
|
||||
"noStaticOnlyClass": "off"
|
||||
},
|
||||
"security": {
|
||||
"noDangerouslySetInnerHtml": "info"
|
||||
@@ -95,11 +94,6 @@
|
||||
"lineEnding": "lf",
|
||||
"formatWithErrors": true
|
||||
},
|
||||
"css": {
|
||||
"parser": {
|
||||
"tailwindDirectives": true
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"semicolons": "asNeeded",
|
||||
@@ -118,21 +112,19 @@
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": true,
|
||||
"includes": [
|
||||
"**",
|
||||
// explicitly force files to be ignored by the scanner with !!
|
||||
"!!**/dist",
|
||||
"!!**/dist-*",
|
||||
"!!**/out",
|
||||
"!!**/evals",
|
||||
"!!**/playwright",
|
||||
"!!**/test-results",
|
||||
"!!**/node_modules",
|
||||
"!!**/webview-ui/build",
|
||||
"!!**/generated",
|
||||
"!!**/proto",
|
||||
"!!**/tests/specs"
|
||||
"!**/dist",
|
||||
"!**/dist-*",
|
||||
"!**/out",
|
||||
"!**/evals",
|
||||
"!**/playwright",
|
||||
"!**/test-results",
|
||||
"!**/node_modules",
|
||||
"!**/webview-ui/build",
|
||||
"!**/generated",
|
||||
"!**/proto",
|
||||
"!**/tests/specs"
|
||||
]
|
||||
},
|
||||
"plugins": [
|
||||
@@ -142,15 +134,14 @@
|
||||
{
|
||||
"includes": [
|
||||
"**",
|
||||
"!!**/dist",
|
||||
"!!**/hosts/vscode/**",
|
||||
"!!**/test/**",
|
||||
"!!**/*.test.ts",
|
||||
"!!src/dev/**",
|
||||
"!!src/extension.ts",
|
||||
"!!src/integrations/git/commit-message-generator.ts",
|
||||
"!!src/integrations/terminal/**",
|
||||
"!!src/core/controller/ui/openWalkthrough.ts"
|
||||
"!**/hosts/vscode/**",
|
||||
"!**/test/**",
|
||||
"!**/*.test.ts",
|
||||
"!src/dev/**",
|
||||
"!src/extension.ts",
|
||||
"!src/integrations/git/commit-message-generator.ts",
|
||||
"!src/integrations/terminal/**",
|
||||
"!src/core/controller/ui/openWalkthrough.ts"
|
||||
],
|
||||
"plugins": [
|
||||
"src/dev/grit/vscode-api.grit"
|
||||
@@ -163,37 +154,35 @@
|
||||
],
|
||||
"includes": [
|
||||
"**",
|
||||
"!!**/esbuild.*",
|
||||
"!!**/*.mts",
|
||||
"!!**/webview-ui/**",
|
||||
"!!**/evals/**",
|
||||
"!!**/standalone/**",
|
||||
"!!**/cli/**",
|
||||
"!!**/e2e/**",
|
||||
"!!**/test/**",
|
||||
"!!**/__tests__/**",
|
||||
"!!**/*.test.ts",
|
||||
"!!**/*.stories.ts",
|
||||
"!!src/dev/**",
|
||||
"!!**/*.mjs",
|
||||
"!!**/*.js",
|
||||
"!!**/scripts/**",
|
||||
"!!**/*.tsx",
|
||||
"!!**/testing-platform/**",
|
||||
// ACP mode must redirect console to stderr - this is intentional
|
||||
"!!cli/src/acp/index.ts"
|
||||
"!**/esbuild.*",
|
||||
"!**/*.mts",
|
||||
"!**/webview-ui/**",
|
||||
"!**/evals/**",
|
||||
"!**/standalone/**",
|
||||
"!**/e2e/**",
|
||||
"!**/test/**",
|
||||
"!**/__tests__/**",
|
||||
"!**/*.test.ts",
|
||||
"!**/*.stories.ts",
|
||||
"!src/dev/**",
|
||||
"!**/*.mjs",
|
||||
"!**/*.js",
|
||||
"!**/scripts/**",
|
||||
"!**/*.tsx",
|
||||
"!**/testing-platform/**",
|
||||
"!**/cli-ts/src/acp/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"includes": [
|
||||
"**",
|
||||
"!!src/core/storage/state-migrations.ts",
|
||||
"!!src/core/storage/FileContextTracker.ts",
|
||||
"!!src/core/context/context-tracking/FileContextTracker.ts",
|
||||
"!!src/common.ts",
|
||||
"!!src/services/logging/distinctId.ts",
|
||||
"!!src/core/storage/utils/state-helpers.ts",
|
||||
"!!src/extension.ts"
|
||||
"!src/core/storage/state-migrations.ts",
|
||||
"!src/core/storage/FileContextTracker.ts",
|
||||
"!src/core/context/context-tracking/FileContextTracker.ts",
|
||||
"!src/common.ts",
|
||||
"!src/services/logging/distinctId.ts",
|
||||
"!src/core/storage/utils/state-helpers.ts",
|
||||
"!src/extension.ts"
|
||||
],
|
||||
"plugins": [
|
||||
"src/dev/grit/use-cache-service.grit"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Cline CLI
|
||||
# Cline CLI (TypeScript)
|
||||
|
||||
The official CLI for Cline. Run Cline tasks directly from the terminal with the same underlying functionality as the VS Code extension.
|
||||
A TypeScript CLI implementation of Cline that reuses the core TypeScript codebase. This allows you to run Cline tasks directly from the terminal while sharing the same underlying functionality as the VS Code extension.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -27,8 +27,23 @@ npm run install:all
|
||||
# Ensure protos are generated
|
||||
npm run protos
|
||||
|
||||
# Build the CLI
|
||||
npm run build:cli
|
||||
```
|
||||
|
||||
Or install the CLI globally:
|
||||
|
||||
```bash
|
||||
# Install all dependencies first
|
||||
npm run install:all
|
||||
|
||||
# Ensure protos are generated
|
||||
npm run protos
|
||||
|
||||
# Build and link the CLI globally
|
||||
npm run cli:link
|
||||
cd cli-ts
|
||||
npm install
|
||||
npm run link
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -188,51 +203,54 @@ These options are available for the default command (running a task directly):
|
||||
|
||||
## Development
|
||||
|
||||
### Quick Start
|
||||
For active development, at the root of this repo:
|
||||
|
||||
1. **Initial setup:**
|
||||
```bash
|
||||
npm run install:all
|
||||
npm run protos
|
||||
```
|
||||
|
||||
2. **Make changes to cli-ts:**
|
||||
```bash
|
||||
npm run cli:dev
|
||||
# or
|
||||
cd cli-ts && npm run dev
|
||||
```
|
||||
|
||||
3. **Test your changes:**
|
||||
```bash
|
||||
cline [your-command] # In a new Terminal
|
||||
```
|
||||
|
||||
4. **When done:**
|
||||
```bash
|
||||
npm run cli:unlink
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
# 1. Install all dependencies (root, webview-ui, cli)
|
||||
npm run install:all
|
||||
cd cli-ts
|
||||
|
||||
# 2. Build and link globally so you can run `cline` from anywhere
|
||||
npm run cli:link
|
||||
# Development build with source maps
|
||||
npm run build
|
||||
|
||||
# 3. Test it
|
||||
cline --help
|
||||
# Production build (minified)
|
||||
npm run build:production
|
||||
```
|
||||
|
||||
### Scripts
|
||||
|
||||
Run these from the repository root:
|
||||
|
||||
| Script | Description |
|
||||
|--------|-------------|
|
||||
| `npm run install:all` | Install deps for root, webview-ui, and cli |
|
||||
| `npm run cli:build` | Generate protos and build CLI |
|
||||
| `npm run cli:build:production` | Production build (minified) |
|
||||
| `npm run cli:link` | Build and `npm link` so you can run `cline` from anywhere |
|
||||
| `npm run cli:unlink` | Remove the global `cline` symlink |
|
||||
| `npm run cli:dev` | Link + watch mode for development |
|
||||
| `npm run cli:watch` | Watch mode only (no initial build) |
|
||||
| `npm run cli:test` | Run CLI tests |
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. Run `npm run cli:dev` - this links the CLI globally and starts watch mode
|
||||
2. Make changes to files in `cli/src/`
|
||||
3. The build automatically rebuilds on save
|
||||
4. Test your changes by running `cline` in another terminal
|
||||
5. When done, run `npm run cli:unlink` to clean up
|
||||
|
||||
### Proto Generation
|
||||
|
||||
The CLI uses proto-generated types for message passing (same as the VS Code extension). If you modify any `.proto` files, run:
|
||||
### Watch Mode
|
||||
|
||||
```bash
|
||||
npm run protos
|
||||
npm run watch
|
||||
```
|
||||
|
||||
This generates TypeScript types in `src/generated/` that both the CLI and extension use.
|
||||
### Type Checking
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
## Publish
|
||||
|
||||
@@ -250,7 +268,7 @@ npm run update-brew-formula
|
||||
```bash
|
||||
# Create a local tap
|
||||
brew tap-new cline/local
|
||||
cp ./cli/cline.rb "$(brew --repository)/Library/Taps/cline/homebrew-local/Formula/cline.rb"
|
||||
cp ./cli-ts/cline.rb "$(brew --repository)/Library/Taps/cline/homebrew-local/Formula/cline.rb"
|
||||
|
||||
# Build from Source
|
||||
brew install --build-from-source cline/local/cline
|
||||
@@ -271,50 +289,19 @@ git push
|
||||
|
||||
## Architecture
|
||||
|
||||
### How It Works
|
||||
The CLI reuses the core Cline TypeScript codebase:
|
||||
|
||||
The CLI directly imports and reuses the core Cline TypeScript codebase (the same code that powers the VS Code extension). This means feature parity is easy to maintain - when core gets updated, the CLI automatically benefits.
|
||||
- **Controller** (`@core/controller`): Manages task lifecycle and state
|
||||
- **Task** (`@core/task`): Executes Cline tasks using the AI API
|
||||
- **StateManager** (`@core/storage`): Handles persistent state storage
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ CLI (cli/) │
|
||||
│ - React Ink terminal UI │
|
||||
│ - Command parsing (commander) │
|
||||
│ - Terminal-specific adapters │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ direct imports
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Core (src/core/) │
|
||||
│ - Controller: task lifecycle, state management │
|
||||
│ - Task: AI API calls, tool execution │
|
||||
│ - StateManager: persistent storage │
|
||||
│ - Proto types: message definitions │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
CLI-specific implementations:
|
||||
|
||||
Unlike a client-server architecture, the CLI runs everything in a single Node.js process. The "host bridge" pattern provides terminal-appropriate implementations for things the VS Code extension would handle differently (clipboard, file dialogs, etc.).
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/index.ts` | Entry point, command definitions |
|
||||
| `src/components/App.tsx` | Main React Ink app |
|
||||
| `src/components/ChatView.tsx` | Task conversation UI |
|
||||
| `src/controllers/CliWebviewProvider.ts` | Bridges core messages to terminal output |
|
||||
| `src/vscode-context.ts` | Mock VS Code extension context for core compatibility |
|
||||
| `src/vscode-shim.ts` | Shims for VS Code APIs that core depends on |
|
||||
| `src/constants/colors.ts` | Terminal color definitions |
|
||||
|
||||
### React Ink
|
||||
|
||||
The CLI uses [React Ink](https://github.com/vadimdemedes/ink) for its terminal UI. This lets us build the interface with React components that render to the terminal. Key patterns:
|
||||
|
||||
- Components in `src/components/` render terminal UI
|
||||
- Hooks in `src/hooks/` manage terminal-specific state (size, scrolling)
|
||||
- The `useStateSubscriber` hook subscribes to core state changes
|
||||
- `cli-host-bridge.ts`: CLI implementations of host bridge services
|
||||
- `cli-webview-provider.ts`: WebviewProvider that outputs to terminal
|
||||
- `cli-comment-review.ts`: Comment review controller for terminal
|
||||
- `vscode-context.ts`: Mock VSCode extension context
|
||||
- `display.ts`: Terminal output formatting utilities
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -327,39 +314,41 @@ The CLI stores its data in `~/.cline/data/` by default:
|
||||
|
||||
Override with the `--config` option or `CLINE_DIR` environment variable.
|
||||
|
||||
## Comparison with Go CLI
|
||||
|
||||
This TypeScript CLI differs from the Go CLI (`cli/` directory):
|
||||
|
||||
| Feature | Go CLI | TypeScript CLI |
|
||||
|---------|--------|----------------|
|
||||
| Language | Go | TypeScript |
|
||||
| Core sharing | Uses gRPC to communicate | Direct imports |
|
||||
| Startup time | Fast | Moderate |
|
||||
| Dependencies | Standalone binary | Requires Node.js |
|
||||
| Best for | Production deployment | Development, debugging |
|
||||
|
||||
Choose the TypeScript CLI when you need to debug or modify the core Cline logic. Choose the Go CLI for production deployment with faster startup.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Errors
|
||||
|
||||
If you encounter build errors:
|
||||
If you encounter build errors, ensure you've:
|
||||
1. Run `npm install` in the repository root
|
||||
2. Run `npm run protos` to generate proto files
|
||||
3. Have all peer dependencies installed
|
||||
|
||||
### Missing Dependencies
|
||||
|
||||
The CLI imports from the parent project. If you see import errors:
|
||||
```bash
|
||||
# Make sure all deps are installed
|
||||
npm run install:all
|
||||
|
||||
# Regenerate proto types
|
||||
cd .. # Go to repository root
|
||||
npm install
|
||||
npm run protos
|
||||
|
||||
# Then rebuild
|
||||
npm run cli:build
|
||||
```
|
||||
|
||||
### "command not found: cline"
|
||||
|
||||
The CLI isn't linked globally. Run:
|
||||
### Permission Denied
|
||||
|
||||
Make the CLI executable:
|
||||
```bash
|
||||
npm run cli:link
|
||||
```
|
||||
|
||||
### Changes Not Reflected
|
||||
|
||||
If your code changes aren't showing up:
|
||||
|
||||
1. Make sure watch mode is running (`npm run cli:dev`)
|
||||
2. Check for TypeScript errors in the watch output
|
||||
3. Try unlinking and relinking: `npm run cli:unlink && npm run cli:link`
|
||||
|
||||
### Import Errors from Core
|
||||
|
||||
The CLI imports from `@core/`, `@shared/`, etc. These paths are defined in the root `tsconfig.json`. If you see import errors, make sure you're building from the repo root, not from inside `cli/`.
|
||||
chmod +x dist/cli.js
|
||||
```
|
||||
@@ -7,7 +7,6 @@ class Cline < Formula
|
||||
license "Apache-2.0"
|
||||
|
||||
depends_on "node@20"
|
||||
depends_on "ripgrep"
|
||||
|
||||
def install
|
||||
system "npm", "install", *std_npm_args(prefix: false)
|
||||
@@ -0,0 +1,272 @@
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import dotenv from "dotenv"
|
||||
import * as esbuild from "esbuild"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const rootDir = path.resolve(__dirname, "..")
|
||||
|
||||
// Load .env from repo root
|
||||
dotenv.config({ path: path.join(rootDir, ".env") })
|
||||
|
||||
const production = process.argv.includes("--production")
|
||||
const watch = process.argv.includes("--watch")
|
||||
|
||||
/**
|
||||
* Plugin to resolve path aliases from the parent project
|
||||
*/
|
||||
const aliasResolverPlugin: esbuild.Plugin = {
|
||||
name: "alias-resolver",
|
||||
setup(build) {
|
||||
const aliases = {
|
||||
"@": path.resolve(rootDir, "src"),
|
||||
"@core": path.resolve(rootDir, "src/core"),
|
||||
"@integrations": path.resolve(rootDir, "src/integrations"),
|
||||
"@services": path.resolve(rootDir, "src/services"),
|
||||
"@shared": path.resolve(rootDir, "src/shared"),
|
||||
"@utils": path.resolve(rootDir, "src/utils"),
|
||||
"@packages": path.resolve(rootDir, "src/packages"),
|
||||
"@hosts": path.resolve(rootDir, "src/hosts"),
|
||||
"@generated": path.resolve(rootDir, "src/generated"),
|
||||
"@api": path.resolve(rootDir, "src/core/api"),
|
||||
}
|
||||
|
||||
// For each alias entry, create a resolver
|
||||
Object.entries(aliases).forEach(([alias, aliasPath]) => {
|
||||
const aliasRegex = new RegExp(`^${alias}($|/.*)`)
|
||||
build.onResolve({ filter: aliasRegex }, (args) => {
|
||||
const importPath = args.path.replace(alias, aliasPath)
|
||||
|
||||
// First, check if the path exists as is
|
||||
if (fs.existsSync(importPath)) {
|
||||
const stats = fs.statSync(importPath)
|
||||
if (stats.isDirectory()) {
|
||||
// If it's a directory, try to find index files
|
||||
const extensions = [".ts", ".tsx", ".js", ".jsx"]
|
||||
for (const ext of extensions) {
|
||||
const indexFile = path.join(importPath, `index${ext}`)
|
||||
if (fs.existsSync(indexFile)) {
|
||||
return { path: indexFile }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// It's a file that exists, so return it
|
||||
return { path: importPath }
|
||||
}
|
||||
}
|
||||
|
||||
// If the path doesn't exist, try appending extensions
|
||||
const extensions = [".ts", ".tsx", ".js", ".jsx"]
|
||||
for (const ext of extensions) {
|
||||
const pathWithExtension = `${importPath}${ext}`
|
||||
if (fs.existsSync(pathWithExtension)) {
|
||||
return { path: pathWithExtension }
|
||||
}
|
||||
}
|
||||
|
||||
// Handle .js -> .ts extension mapping (common in ESM TypeScript projects)
|
||||
if (importPath.endsWith(".js")) {
|
||||
const tsPath = importPath.replace(/\.js$/, ".ts")
|
||||
if (fs.existsSync(tsPath)) {
|
||||
return { path: tsPath }
|
||||
}
|
||||
const tsxPath = importPath.replace(/\.js$/, ".tsx")
|
||||
if (fs.existsSync(tsxPath)) {
|
||||
return { path: tsxPath }
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing worked, return the original path and let esbuild handle the error
|
||||
return { path: importPath }
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin to redirect vscode imports to our shim
|
||||
*/
|
||||
const vscodeStubPlugin: esbuild.Plugin = {
|
||||
name: "vscode-stub",
|
||||
setup(build) {
|
||||
// Redirect 'vscode' imports to our shim
|
||||
build.onResolve({ filter: /^vscode$/ }, () => {
|
||||
return { path: path.join(__dirname, "src", "vscode-shim.ts") }
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const esbuildProblemMatcherPlugin: esbuild.Plugin = {
|
||||
name: "esbuild-problem-matcher",
|
||||
setup(build) {
|
||||
build.onStart(() => {
|
||||
console.log("[cli-ts esbuild] Build started...")
|
||||
})
|
||||
build.onEnd((result) => {
|
||||
result.errors.forEach(({ text, location }) => {
|
||||
console.error(`✘ [ERROR] ${text}`)
|
||||
if (location) {
|
||||
console.error(` ${location.file}:${location.line}:${location.column}:`)
|
||||
}
|
||||
})
|
||||
console.log("[cli-ts esbuild] Build finished")
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// Plugin to stub out optional devtools module
|
||||
const stubOptionalModulesPlugin: esbuild.Plugin = {
|
||||
name: "stub-optional-modules",
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^react-devtools-core$/ }, () => {
|
||||
return { path: path.join(__dirname, "src", "stub-devtools.js"), external: false }
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const copyWasmFiles: esbuild.Plugin = {
|
||||
name: "copy-wasm-files",
|
||||
setup(build) {
|
||||
build.onEnd(() => {
|
||||
const destDir = path.join(__dirname, "dist")
|
||||
|
||||
// Ensure dist directory exists
|
||||
if (!fs.existsSync(destDir)) {
|
||||
fs.mkdirSync(destDir, { recursive: true })
|
||||
}
|
||||
|
||||
// tree sitter
|
||||
const sourceDir = path.join(rootDir, "node_modules", "web-tree-sitter")
|
||||
|
||||
// Copy tree-sitter.wasm
|
||||
const treeSitterWasm = path.join(sourceDir, "tree-sitter.wasm")
|
||||
if (fs.existsSync(treeSitterWasm)) {
|
||||
fs.copyFileSync(treeSitterWasm, path.join(destDir, "tree-sitter.wasm"))
|
||||
}
|
||||
|
||||
// Copy language-specific WASM files
|
||||
const languageWasmDir = path.join(rootDir, "node_modules", "tree-sitter-wasms", "out")
|
||||
const languages = [
|
||||
"typescript",
|
||||
"tsx",
|
||||
"python",
|
||||
"rust",
|
||||
"javascript",
|
||||
"go",
|
||||
"cpp",
|
||||
"c",
|
||||
"c_sharp",
|
||||
"ruby",
|
||||
"java",
|
||||
"php",
|
||||
"swift",
|
||||
"kotlin",
|
||||
]
|
||||
|
||||
if (fs.existsSync(languageWasmDir)) {
|
||||
languages.forEach((lang) => {
|
||||
const filename = `tree-sitter-${lang}.wasm`
|
||||
const sourcePath = path.join(languageWasmDir, filename)
|
||||
if (fs.existsSync(sourcePath)) {
|
||||
fs.copyFileSync(sourcePath, path.join(destDir, filename))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const buildEnvVars: Record<string, string> = {
|
||||
"process.env.IS_STANDALONE": JSON.stringify("true"),
|
||||
"process.env.IS_CLI": JSON.stringify("true"),
|
||||
}
|
||||
|
||||
const buildTimeEnvs = [
|
||||
"TELEMETRY_SERVICE_API_KEY",
|
||||
"ERROR_SERVICE_API_KEY",
|
||||
"POSTHOG_TELEMETRY_ENABLED",
|
||||
"OTEL_TELEMETRY_ENABLED",
|
||||
"OTEL_LOGS_EXPORTER",
|
||||
"OTEL_METRICS_EXPORTER",
|
||||
"OTEL_EXPORTER_OTLP_PROTOCOL",
|
||||
"OTEL_EXPORTER_OTLP_ENDPOINT",
|
||||
"OTEL_EXPORTER_OTLP_HEADERS",
|
||||
"OTEL_METRIC_EXPORT_INTERVAL",
|
||||
"CLINE_ENVIRONMENT",
|
||||
]
|
||||
|
||||
buildTimeEnvs.forEach((envVar) => {
|
||||
if (process.env[envVar]) {
|
||||
console.log(`[cli-ts esbuild] ${envVar} env var is set`)
|
||||
buildEnvVars[`process.env.${envVar}`] = JSON.stringify(process.env[envVar])
|
||||
}
|
||||
})
|
||||
|
||||
if (production) {
|
||||
buildEnvVars["process.env.IS_DEV"] = "false"
|
||||
}
|
||||
|
||||
const config: esbuild.BuildOptions = {
|
||||
entryPoints: [path.join(__dirname, "src", "index.ts")],
|
||||
bundle: true,
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
logLevel: "silent",
|
||||
define: buildEnvVars,
|
||||
tsconfig: path.join(__dirname, "tsconfig.json"),
|
||||
plugins: [copyWasmFiles, aliasResolverPlugin, vscodeStubPlugin, stubOptionalModulesPlugin, esbuildProblemMatcherPlugin],
|
||||
format: "esm",
|
||||
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",
|
||||
"grpc-health-check",
|
||||
"better-sqlite3",
|
||||
"ink",
|
||||
"ink-spinner",
|
||||
"ink-picture",
|
||||
"react",
|
||||
"aws4fetch",
|
||||
"pino",
|
||||
"pino-roll",
|
||||
],
|
||||
supported: { "top-level-await": true },
|
||||
banner: {
|
||||
js: `#!/usr/bin/env node
|
||||
// Suppress all Node.js warnings (deprecation, experimental, etc.)
|
||||
process.emitWarning = () => {};
|
||||
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) {
|
||||
await ctx.watch()
|
||||
console.log("[cli-ts] Watching for changes...")
|
||||
} else {
|
||||
await ctx.rebuild()
|
||||
await ctx.dispose()
|
||||
|
||||
// Make the output executable
|
||||
const outfile = path.join(__dirname, "dist", "cli.mjs")
|
||||
if (fs.existsSync(outfile)) {
|
||||
fs.chmodSync(outfile, "755")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
})
|
||||
Generated
+2978
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"name": "@cline/cli",
|
||||
"version": "2.0.0",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"main": "dist/cli.mjs",
|
||||
"bin": {
|
||||
"cline": "./dist/cli.mjs"
|
||||
},
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "npm run build:production",
|
||||
"package:brew": "node ./scripts/update-brew-formula.mts",
|
||||
"package": "npm pack --pack-destination ./dist",
|
||||
"build": "node esbuild.mts",
|
||||
"build:production": "node esbuild.mts --production",
|
||||
"watch": "node esbuild.mts --watch",
|
||||
"dev": "IS_DEV=true && npm run link && npm run watch ; npm run unlink",
|
||||
"clean": "rimraf dist",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"link": "npm run build && npm link",
|
||||
"unlink": "npm unlink -g @cline/cli && npm unlink -g cline",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run"
|
||||
},
|
||||
"keywords": [
|
||||
"cline",
|
||||
"claude",
|
||||
"dev",
|
||||
"mcp",
|
||||
"openrouter",
|
||||
"coding",
|
||||
"agent",
|
||||
"autonomous",
|
||||
"chatgpt",
|
||||
"sonnet",
|
||||
"ai",
|
||||
"llama",
|
||||
"cli"
|
||||
],
|
||||
"author": {
|
||||
"name": "Cline Bot Inc."
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cline/cline"
|
||||
},
|
||||
"homepage": "https://cline.bot",
|
||||
"bugs": {
|
||||
"url": "https://github.com/cline/cline/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.x",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@types/react": "^19.2.9",
|
||||
"dotenv": "^16.4.5",
|
||||
"esbuild": "^0.25.0",
|
||||
"ink-testing-library": "^4.0.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "^5.4.5",
|
||||
"vitest": "^4.0.17"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.13.1",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.1.0",
|
||||
"ink": "npm:@jrichman/ink@6.4.7",
|
||||
"ink-picture": "^1.3.3",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"ora": "^8.0.1",
|
||||
"nanoid": "^5.1.6",
|
||||
"pino": "^10.0.0",
|
||||
"pino-roll": "^4.0.0",
|
||||
"prompts": "^2.4.2",
|
||||
"react": "^19.2.3"
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ import { HostProvider } from "@/hosts/host-provider"
|
||||
import { FileEditProvider } from "@/integrations/editor/FileEditProvider"
|
||||
import { detectEncoding } from "@/integrations/misc/extract-text"
|
||||
import type { FileDiagnostics } from "@/shared/proto/index.cline"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
/**
|
||||
* A function that resolves the current session ID.
|
||||
@@ -44,6 +43,7 @@ export class ACPDiffViewProvider extends FileEditProvider {
|
||||
private readonly connection: acp.AgentSideConnection
|
||||
private readonly clientCapabilities: acp.ClientCapabilities | undefined
|
||||
private readonly sessionIdResolver: SessionIdResolver
|
||||
private readonly debug: boolean
|
||||
|
||||
/**
|
||||
* Creates a new ACPDiffViewProvider.
|
||||
@@ -51,16 +51,19 @@ export class ACPDiffViewProvider extends FileEditProvider {
|
||||
* @param connection - The ACP agent-side connection for making requests
|
||||
* @param clientCapabilities - The client's advertised capabilities
|
||||
* @param sessionIdResolver - A function that returns the current session ID
|
||||
* @param debug - Whether to enable debug logging
|
||||
*/
|
||||
constructor(
|
||||
connection: acp.AgentSideConnection,
|
||||
clientCapabilities: acp.ClientCapabilities | undefined,
|
||||
sessionIdResolver: SessionIdResolver,
|
||||
debug: boolean = false,
|
||||
) {
|
||||
super()
|
||||
this.connection = connection
|
||||
this.clientCapabilities = clientCapabilities
|
||||
this.sessionIdResolver = sessionIdResolver
|
||||
this.debug = debug
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,7 +101,9 @@ export class ACPDiffViewProvider extends FileEditProvider {
|
||||
override async open(relPath: string, options?: { displayPath?: string }): Promise<void> {
|
||||
// If we can't read files via ACP, fall back to FileEditProvider
|
||||
if (!this.canReadFile()) {
|
||||
Logger.debug("[ACPDiffViewProvider] Client does not support fs.readTextFile, falling back to local fs")
|
||||
if (this.debug) {
|
||||
console.error("[ACPDiffViewProvider] Client does not support fs.readTextFile, falling back to local fs")
|
||||
}
|
||||
return super.open(relPath, options)
|
||||
}
|
||||
|
||||
@@ -124,7 +129,9 @@ export class ACPDiffViewProvider extends FileEditProvider {
|
||||
|
||||
// Read file content via ACP
|
||||
try {
|
||||
Logger.debug("[ACPDiffViewProvider] Reading file via ACP:", this.absolutePath)
|
||||
if (this.debug) {
|
||||
console.error("[ACPDiffViewProvider] Reading file via ACP:", this.absolutePath)
|
||||
}
|
||||
|
||||
const response = await this.connection.readTextFile({
|
||||
sessionId: this.getSessionId(),
|
||||
@@ -135,10 +142,14 @@ export class ACPDiffViewProvider extends FileEditProvider {
|
||||
// ACP always returns UTF-8 text content
|
||||
this.fileEncoding = "utf8"
|
||||
|
||||
Logger.debug("[ACPDiffViewProvider] Read file successfully, length:", response.content.length)
|
||||
if (this.debug) {
|
||||
console.error("[ACPDiffViewProvider] Read file successfully, length:", response.content.length)
|
||||
}
|
||||
} catch (error) {
|
||||
// If ACP read fails, fall back to local fs
|
||||
Logger.debug("[ACPDiffViewProvider] ACP read failed, falling back to local fs:", error)
|
||||
if (this.debug) {
|
||||
console.error("[ACPDiffViewProvider] ACP read failed, falling back to local fs:", error)
|
||||
}
|
||||
|
||||
const fileBuffer = await fs.readFile(this.absolutePath!)
|
||||
this.fileEncoding = await detectEncoding(fileBuffer)
|
||||
@@ -214,7 +225,9 @@ export class ACPDiffViewProvider extends FileEditProvider {
|
||||
protected override async saveDocument(): Promise<Boolean> {
|
||||
// If we can't write files via ACP, fall back to FileEditProvider
|
||||
if (!this.canWriteFile()) {
|
||||
Logger.debug("[ACPDiffViewProvider] Client does not support fs.writeTextFile, falling back to local fs")
|
||||
if (this.debug) {
|
||||
console.error("[ACPDiffViewProvider] Client does not support fs.writeTextFile, falling back to local fs")
|
||||
}
|
||||
return super.saveDocument()
|
||||
}
|
||||
|
||||
@@ -224,10 +237,12 @@ export class ACPDiffViewProvider extends FileEditProvider {
|
||||
}
|
||||
|
||||
try {
|
||||
Logger.debug("[ACPDiffViewProvider] Writing file via ACP:", {
|
||||
path: this.absolutePath,
|
||||
contentLength: content.length,
|
||||
})
|
||||
if (this.debug) {
|
||||
console.error("[ACPDiffViewProvider] Writing file via ACP:", {
|
||||
path: this.absolutePath,
|
||||
contentLength: content.length,
|
||||
})
|
||||
}
|
||||
|
||||
await this.connection.writeTextFile({
|
||||
sessionId: this.getSessionId(),
|
||||
@@ -235,12 +250,16 @@ export class ACPDiffViewProvider extends FileEditProvider {
|
||||
content: content,
|
||||
})
|
||||
|
||||
Logger.debug("[ACPDiffViewProvider] Write file successfully")
|
||||
if (this.debug) {
|
||||
console.error("[ACPDiffViewProvider] Write file successfully")
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
// If ACP write fails, fall back to local fs
|
||||
Logger.debug("[ACPDiffViewProvider] ACP write failed, falling back to local fs:", error)
|
||||
if (this.debug) {
|
||||
console.error("[ACPDiffViewProvider] ACP write failed, falling back to local fs:", error)
|
||||
}
|
||||
|
||||
return super.saveDocument()
|
||||
}
|
||||
+151
-65
@@ -18,7 +18,6 @@ import type {
|
||||
import type { HostBridgeClientProvider, StreamingCallbacks } from "@hosts/host-provider-types"
|
||||
import * as proto from "@shared/proto/index"
|
||||
import { ClineClient } from "@/shared/cline"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
/**
|
||||
* Function type that resolves the current session ID.
|
||||
@@ -40,11 +39,24 @@ export type CwdResolver = () => string | undefined
|
||||
* the fs capabilities (readTextFile/writeTextFile).
|
||||
*/
|
||||
class ACPDiffServiceClient implements DiffServiceClientInterface {
|
||||
private readonly debug: boolean
|
||||
|
||||
constructor(
|
||||
_connection: acp.AgentSideConnection,
|
||||
_clientCapabilities: acp.ClientCapabilities | undefined,
|
||||
_sessionIdResolver: SessionIdResolver,
|
||||
debug: boolean = false,
|
||||
) {
|
||||
this.debug = debug
|
||||
}
|
||||
|
||||
async openDiff(_request: proto.host.OpenDiffRequest): Promise<proto.host.OpenDiffResponse> {
|
||||
// Next phase: Could use ACP client capabilities to open a diff view in the editor.
|
||||
// This would involve sending an ACP extension notification/request to the client
|
||||
// to display a side-by-side diff of the original vs modified content.
|
||||
Logger.debug("[ACPDiffServiceClient] openDiff called (stub)")
|
||||
if (this.debug) {
|
||||
console.error("[ACPDiffServiceClient] openDiff called (stub)")
|
||||
}
|
||||
return proto.host.OpenDiffResponse.create({})
|
||||
}
|
||||
|
||||
@@ -52,49 +64,63 @@ class ACPDiffServiceClient implements DiffServiceClientInterface {
|
||||
// Next phase: Use connection.readTextFile if clientCapabilities.fs.readTextFile is available.
|
||||
// This would read the current document content from the editor, including any unsaved changes.
|
||||
// For now, return empty content.
|
||||
Logger.debug("[ACPDiffServiceClient] getDocumentText called (stub)", { diffId: request.diffId })
|
||||
if (this.debug) {
|
||||
console.error("[ACPDiffServiceClient] getDocumentText called (stub)", { diffId: request.diffId })
|
||||
}
|
||||
return proto.host.GetDocumentTextResponse.create({ content: "" })
|
||||
}
|
||||
|
||||
async replaceText(_request: proto.host.ReplaceTextRequest): Promise<proto.host.ReplaceTextResponse> {
|
||||
// Next phase: Use connection.writeTextFile if clientCapabilities.fs.writeTextFile is available.
|
||||
// This would replace text in the document at the specified range.
|
||||
Logger.debug("[ACPDiffServiceClient] replaceText called (stub)")
|
||||
if (this.debug) {
|
||||
console.error("[ACPDiffServiceClient] replaceText called (stub)")
|
||||
}
|
||||
return proto.host.ReplaceTextResponse.create({})
|
||||
}
|
||||
|
||||
async scrollDiff(_request: proto.host.ScrollDiffRequest): Promise<proto.host.ScrollDiffResponse> {
|
||||
// Next phase: Send ACP extension notification to scroll the diff view to a specific line.
|
||||
// No visual editor in ACP mode by default, so this is a no-op.
|
||||
Logger.debug("[ACPDiffServiceClient] scrollDiff called (stub)")
|
||||
if (this.debug) {
|
||||
console.error("[ACPDiffServiceClient] scrollDiff called (stub)")
|
||||
}
|
||||
return proto.host.ScrollDiffResponse.create({})
|
||||
}
|
||||
|
||||
async truncateDocument(_request: proto.host.TruncateDocumentRequest): Promise<proto.host.TruncateDocumentResponse> {
|
||||
// Next phase: Read file using readTextFile, truncate content, write back using writeTextFile.
|
||||
// This is used to truncate a document to a specific line count.
|
||||
Logger.debug("[ACPDiffServiceClient] truncateDocument called (stub)")
|
||||
if (this.debug) {
|
||||
console.error("[ACPDiffServiceClient] truncateDocument called (stub)")
|
||||
}
|
||||
return proto.host.TruncateDocumentResponse.create({})
|
||||
}
|
||||
|
||||
async saveDocument(_request: proto.host.SaveDocumentRequest): Promise<proto.host.SaveDocumentResponse> {
|
||||
// Next phase: Use connection.writeTextFile to persist the document to disk.
|
||||
// This saves the current document content to the file system.
|
||||
Logger.debug("[ACPDiffServiceClient] saveDocument called (stub)")
|
||||
if (this.debug) {
|
||||
console.error("[ACPDiffServiceClient] saveDocument called (stub)")
|
||||
}
|
||||
return proto.host.SaveDocumentResponse.create({})
|
||||
}
|
||||
|
||||
async closeAllDiffs(_request: proto.host.CloseAllDiffsRequest): Promise<proto.host.CloseAllDiffsResponse> {
|
||||
// Next phase: Send ACP extension notification to close all diff views in the editor.
|
||||
// No visual diff views in ACP mode by default, so this is a no-op.
|
||||
Logger.debug("[ACPDiffServiceClient] closeAllDiffs called (stub)")
|
||||
if (this.debug) {
|
||||
console.error("[ACPDiffServiceClient] closeAllDiffs called (stub)")
|
||||
}
|
||||
return proto.host.CloseAllDiffsResponse.create({})
|
||||
}
|
||||
|
||||
async openMultiFileDiff(_request: proto.host.OpenMultiFileDiffRequest): Promise<proto.host.OpenMultiFileDiffResponse> {
|
||||
// Next phase: Send ACP extension notification to open a multi-file diff view.
|
||||
// This would display changes across multiple files in the editor.
|
||||
Logger.debug("[ACPDiffServiceClient] openMultiFileDiff called (stub)")
|
||||
if (this.debug) {
|
||||
console.error("[ACPDiffServiceClient] openMultiFileDiff called (stub)")
|
||||
}
|
||||
return proto.host.OpenMultiFileDiffResponse.create({})
|
||||
}
|
||||
}
|
||||
@@ -106,24 +132,35 @@ class ACPDiffServiceClient implements DiffServiceClientInterface {
|
||||
* Most operations are stubs that will be implemented using ACP extension methods.
|
||||
*/
|
||||
class ACPEnvServiceClient implements EnvServiceClientInterface {
|
||||
private readonly debug: boolean
|
||||
private readonly version: string
|
||||
|
||||
constructor(_clientCapabilities: acp.ClientCapabilities | undefined, _sessionIdResolver: SessionIdResolver, version: string) {
|
||||
constructor(
|
||||
_connection: acp.AgentSideConnection,
|
||||
_clientCapabilities: acp.ClientCapabilities | undefined,
|
||||
_sessionIdResolver: SessionIdResolver,
|
||||
debug: boolean = false,
|
||||
version: string = "1.0.0",
|
||||
) {
|
||||
this.debug = debug
|
||||
this.version = version
|
||||
}
|
||||
|
||||
async debugLog(request: proto.cline.StringRequest): Promise<proto.cline.Empty> {
|
||||
Logger.debug(request.value)
|
||||
return proto.cline.Empty.create()
|
||||
}
|
||||
|
||||
async clipboardWriteText(_request: proto.cline.StringRequest): Promise<proto.cline.Empty> {
|
||||
Logger.debug("[ACPEnvServiceClient] clipboardWriteText called (stub)")
|
||||
// Next phase: Send ACP extension request for clipboard write.
|
||||
// This would copy text to the system clipboard via the ACP client.
|
||||
if (this.debug) {
|
||||
console.error("[ACPEnvServiceClient] clipboardWriteText called (stub)")
|
||||
}
|
||||
return proto.cline.Empty.create()
|
||||
}
|
||||
|
||||
async clipboardReadText(_request: proto.cline.EmptyRequest): Promise<proto.cline.String> {
|
||||
Logger.debug("[ACPEnvServiceClient] clipboardReadText called (stub)")
|
||||
// Next phase: Send ACP extension request for clipboard read.
|
||||
// This would read text from the system clipboard via the ACP client.
|
||||
if (this.debug) {
|
||||
console.error("[ACPEnvServiceClient] clipboardReadText called (stub)")
|
||||
}
|
||||
return proto.cline.String.create({ value: "" })
|
||||
}
|
||||
|
||||
@@ -137,7 +174,11 @@ class ACPEnvServiceClient implements EnvServiceClientInterface {
|
||||
}
|
||||
|
||||
async getIdeRedirectUri(_request: proto.cline.EmptyRequest): Promise<proto.cline.String> {
|
||||
Logger.debug("[ACPEnvServiceClient] getIdeRedirectUri called (stub)")
|
||||
// Next phase: Query ACP client for IDE redirect URI for OAuth flows.
|
||||
// ACP mode doesn't typically have an IDE redirect URI.
|
||||
if (this.debug) {
|
||||
console.error("[ACPEnvServiceClient] getIdeRedirectUri called (stub)")
|
||||
}
|
||||
return proto.cline.String.create({ value: "" })
|
||||
}
|
||||
|
||||
@@ -165,16 +206,8 @@ class ACPEnvServiceClient implements EnvServiceClientInterface {
|
||||
async shutdown(_request: proto.cline.EmptyRequest): Promise<proto.cline.Empty> {
|
||||
// Next phase: Graceful ACP connection shutdown.
|
||||
// This would cleanly close the ACP connection and release resources.
|
||||
Logger.debug("[ACPEnvServiceClient] shutdown called (stub)")
|
||||
return proto.cline.Empty.create()
|
||||
}
|
||||
|
||||
async openExternal(request: proto.cline.StringRequest): Promise<proto.cline.Empty> {
|
||||
const url = request.value || ""
|
||||
if (url) {
|
||||
Logger.debug(`[ACPEnvServiceClient] openExternal: ${url}`)
|
||||
const { openUrlInBrowser } = await import("../utils/browser")
|
||||
await openUrlInBrowser(url)
|
||||
if (this.debug) {
|
||||
console.error("[ACPEnvServiceClient] shutdown called (stub)")
|
||||
}
|
||||
return proto.cline.Empty.create()
|
||||
}
|
||||
@@ -187,12 +220,23 @@ class ACPEnvServiceClient implements EnvServiceClientInterface {
|
||||
* Most operations are stubs that will be implemented using ACP extension methods.
|
||||
*/
|
||||
class ACPWindowServiceClient implements WindowServiceClientInterface {
|
||||
constructor(_clientCapabilities: acp.ClientCapabilities | undefined, _sessionIdResolver: SessionIdResolver) {}
|
||||
private readonly debug: boolean
|
||||
|
||||
constructor(
|
||||
_connection: acp.AgentSideConnection,
|
||||
_clientCapabilities: acp.ClientCapabilities | undefined,
|
||||
_sessionIdResolver: SessionIdResolver,
|
||||
debug: boolean = false,
|
||||
) {
|
||||
this.debug = debug
|
||||
}
|
||||
|
||||
async showTextDocument(request: proto.host.ShowTextDocumentRequest): Promise<proto.host.TextEditorInfo> {
|
||||
// Next phase: Send ACP extension request to open document in the editor.
|
||||
// This would tell the ACP client to open the specified file.
|
||||
Logger.debug("[ACPWindowServiceClient] showTextDocument called (stub)", { path: request.path })
|
||||
if (this.debug) {
|
||||
console.error("[ACPWindowServiceClient] showTextDocument called (stub)", { path: request.path })
|
||||
}
|
||||
return proto.host.TextEditorInfo.create({
|
||||
documentPath: request.path,
|
||||
})
|
||||
@@ -201,66 +245,84 @@ class ACPWindowServiceClient implements WindowServiceClientInterface {
|
||||
async showOpenDialogue(_request: proto.host.ShowOpenDialogueRequest): Promise<proto.host.SelectedResources> {
|
||||
// Next phase: Send ACP extension request for file picker dialog.
|
||||
// This would display a file open dialog in the ACP client.
|
||||
Logger.debug("[ACPWindowServiceClient] showOpenDialogue called (stub)")
|
||||
if (this.debug) {
|
||||
console.error("[ACPWindowServiceClient] showOpenDialogue called (stub)")
|
||||
}
|
||||
return proto.host.SelectedResources.create({ paths: [] })
|
||||
}
|
||||
|
||||
async showMessage(request: proto.host.ShowMessageRequest): Promise<proto.host.SelectedResponse> {
|
||||
// Next phase: Send ACP extension notification to show message in the editor.
|
||||
// This would display an information/warning/error message to the user.
|
||||
Logger.debug("[ACPWindowServiceClient] showMessage called (stub)", {
|
||||
message: request.message,
|
||||
type: request.type,
|
||||
})
|
||||
if (this.debug) {
|
||||
console.error("[ACPWindowServiceClient] showMessage called (stub)", {
|
||||
message: request.message,
|
||||
type: request.type,
|
||||
})
|
||||
}
|
||||
return proto.host.SelectedResponse.create({})
|
||||
}
|
||||
|
||||
async showInputBox(_request: proto.host.ShowInputBoxRequest): Promise<proto.host.ShowInputBoxResponse> {
|
||||
// Next phase: Send ACP extension request for input dialog.
|
||||
// This would display an input box for user text entry.
|
||||
Logger.debug("[ACPWindowServiceClient] showInputBox called (stub)")
|
||||
if (this.debug) {
|
||||
console.error("[ACPWindowServiceClient] showInputBox called (stub)")
|
||||
}
|
||||
return proto.host.ShowInputBoxResponse.create({ response: "" })
|
||||
}
|
||||
|
||||
async showSaveDialog(_request: proto.host.ShowSaveDialogRequest): Promise<proto.host.ShowSaveDialogResponse> {
|
||||
// Next phase: Send ACP extension request for save dialog.
|
||||
// This would display a file save dialog in the ACP client.
|
||||
Logger.debug("[ACPWindowServiceClient] showSaveDialog called (stub)")
|
||||
if (this.debug) {
|
||||
console.error("[ACPWindowServiceClient] showSaveDialog called (stub)")
|
||||
}
|
||||
return proto.host.ShowSaveDialogResponse.create({ selectedPath: "" })
|
||||
}
|
||||
|
||||
async openFile(request: proto.host.OpenFileRequest): Promise<proto.host.OpenFileResponse> {
|
||||
// Next phase: Send ACP extension request to open file in the editor.
|
||||
// This would open the specified file in the ACP client's editor.
|
||||
Logger.debug("[ACPWindowServiceClient] openFile called (stub)", { filePath: request.filePath })
|
||||
if (this.debug) {
|
||||
console.error("[ACPWindowServiceClient] openFile called (stub)", { filePath: request.filePath })
|
||||
}
|
||||
return proto.host.OpenFileResponse.create({})
|
||||
}
|
||||
|
||||
async openSettings(_request: proto.host.OpenSettingsRequest): Promise<proto.host.OpenSettingsResponse> {
|
||||
// Next phase: Send ACP extension request to open settings panel.
|
||||
// This would open the settings/preferences in the ACP client.
|
||||
Logger.debug("[ACPWindowServiceClient] openSettings called (stub)")
|
||||
if (this.debug) {
|
||||
console.error("[ACPWindowServiceClient] openSettings called (stub)")
|
||||
}
|
||||
return proto.host.OpenSettingsResponse.create({})
|
||||
}
|
||||
|
||||
async getOpenTabs(_request: proto.host.GetOpenTabsRequest): Promise<proto.host.GetOpenTabsResponse> {
|
||||
// Next phase: Send ACP extension request to list open tabs/documents.
|
||||
// This would return a list of currently open files in the editor.
|
||||
Logger.debug("[ACPWindowServiceClient] getOpenTabs called (stub)")
|
||||
if (this.debug) {
|
||||
console.error("[ACPWindowServiceClient] getOpenTabs called (stub)")
|
||||
}
|
||||
return proto.host.GetOpenTabsResponse.create({ paths: [] })
|
||||
}
|
||||
|
||||
async getVisibleTabs(_request: proto.host.GetVisibleTabsRequest): Promise<proto.host.GetVisibleTabsResponse> {
|
||||
// Next phase: Send ACP extension request to list visible tabs.
|
||||
// This would return a list of visible tabs/panes in the editor.
|
||||
Logger.debug("[ACPWindowServiceClient] getVisibleTabs called (stub)")
|
||||
if (this.debug) {
|
||||
console.error("[ACPWindowServiceClient] getVisibleTabs called (stub)")
|
||||
}
|
||||
return proto.host.GetVisibleTabsResponse.create({ paths: [] })
|
||||
}
|
||||
|
||||
async getActiveEditor(_request: proto.host.GetActiveEditorRequest): Promise<proto.host.GetActiveEditorResponse> {
|
||||
// Next phase: Send ACP extension request to get active editor info.
|
||||
// This would return information about the currently focused editor.
|
||||
Logger.debug("[ACPWindowServiceClient] getActiveEditor called (stub)")
|
||||
if (this.debug) {
|
||||
console.error("[ACPWindowServiceClient] getActiveEditor called (stub)")
|
||||
}
|
||||
return proto.host.GetActiveEditorResponse.create({})
|
||||
}
|
||||
}
|
||||
@@ -274,14 +336,18 @@ class ACPWindowServiceClient implements WindowServiceClientInterface {
|
||||
class ACPWorkspaceServiceClient implements WorkspaceServiceClientInterface {
|
||||
private readonly _clientCapabilities: acp.ClientCapabilities | undefined
|
||||
private readonly cwdResolver: CwdResolver
|
||||
private readonly debug: boolean
|
||||
|
||||
constructor(
|
||||
_connection: acp.AgentSideConnection,
|
||||
clientCapabilities: acp.ClientCapabilities | undefined,
|
||||
_sessionIdResolver: SessionIdResolver,
|
||||
cwdResolver: CwdResolver,
|
||||
debug: boolean = false,
|
||||
) {
|
||||
this._clientCapabilities = clientCapabilities
|
||||
this.cwdResolver = cwdResolver
|
||||
this.debug = debug
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,7 +361,9 @@ class ACPWorkspaceServiceClient implements WorkspaceServiceClientInterface {
|
||||
async getWorkspacePaths(_request: proto.host.GetWorkspacePathsRequest): Promise<proto.host.GetWorkspacePathsResponse> {
|
||||
// Return the current working directory from the resolver.
|
||||
const cwd = this.getCwd()
|
||||
Logger.debug("[ACPWorkspaceServiceClient] getWorkspacePaths called", { cwd })
|
||||
if (this.debug) {
|
||||
console.error("[ACPWorkspaceServiceClient] getWorkspacePaths called", { cwd })
|
||||
}
|
||||
return proto.host.GetWorkspacePathsResponse.create({
|
||||
paths: [cwd],
|
||||
})
|
||||
@@ -306,21 +374,27 @@ class ACPWorkspaceServiceClient implements WorkspaceServiceClientInterface {
|
||||
): Promise<proto.host.SaveOpenDocumentIfDirtyResponse> {
|
||||
// Next phase: Use ACP extension or fs.writeTextFile to save dirty documents.
|
||||
// This would save any unsaved changes in the specified document.
|
||||
Logger.debug("[ACPWorkspaceServiceClient] saveOpenDocumentIfDirty called (stub)")
|
||||
if (this.debug) {
|
||||
console.error("[ACPWorkspaceServiceClient] saveOpenDocumentIfDirty called (stub)")
|
||||
}
|
||||
return proto.host.SaveOpenDocumentIfDirtyResponse.create({})
|
||||
}
|
||||
|
||||
async getDiagnostics(_request: proto.host.GetDiagnosticsRequest): Promise<proto.host.GetDiagnosticsResponse> {
|
||||
// Next phase: Send ACP extension request for diagnostics (errors, warnings).
|
||||
// This would return linting/compilation errors from the ACP client.
|
||||
Logger.debug("[ACPWorkspaceServiceClient] getDiagnostics called (stub)")
|
||||
if (this.debug) {
|
||||
console.error("[ACPWorkspaceServiceClient] getDiagnostics called (stub)")
|
||||
}
|
||||
return proto.host.GetDiagnosticsResponse.create({ fileDiagnostics: [] })
|
||||
}
|
||||
|
||||
async openProblemsPanel(_request: proto.host.OpenProblemsPanelRequest): Promise<proto.host.OpenProblemsPanelResponse> {
|
||||
// Next phase: Send ACP extension notification to open the problems panel.
|
||||
// This would show the diagnostics/problems view in the editor.
|
||||
Logger.debug("[ACPWorkspaceServiceClient] openProblemsPanel called (stub)")
|
||||
if (this.debug) {
|
||||
console.error("[ACPWorkspaceServiceClient] openProblemsPanel called (stub)")
|
||||
}
|
||||
return proto.host.OpenProblemsPanelResponse.create({})
|
||||
}
|
||||
|
||||
@@ -329,7 +403,9 @@ class ACPWorkspaceServiceClient implements WorkspaceServiceClientInterface {
|
||||
): Promise<proto.host.OpenInFileExplorerPanelResponse> {
|
||||
// Next phase: Send ACP extension notification to reveal file in explorer.
|
||||
// This would highlight/reveal the specified path in the file tree.
|
||||
Logger.debug("[ACPWorkspaceServiceClient] openInFileExplorerPanel called (stub)", { path: request.path })
|
||||
if (this.debug) {
|
||||
console.error("[ACPWorkspaceServiceClient] openInFileExplorerPanel called (stub)", { path: request.path })
|
||||
}
|
||||
return proto.host.OpenInFileExplorerPanelResponse.create({})
|
||||
}
|
||||
|
||||
@@ -338,14 +414,18 @@ class ACPWorkspaceServiceClient implements WorkspaceServiceClientInterface {
|
||||
): Promise<proto.host.OpenClineSidebarPanelResponse> {
|
||||
// Next phase: Send ACP extension notification to open Cline sidebar.
|
||||
// This would show the Cline panel/sidebar in the editor.
|
||||
Logger.debug("[ACPWorkspaceServiceClient] openClineSidebarPanel called (stub)")
|
||||
if (this.debug) {
|
||||
console.error("[ACPWorkspaceServiceClient] openClineSidebarPanel called (stub)")
|
||||
}
|
||||
return proto.host.OpenClineSidebarPanelResponse.create({})
|
||||
}
|
||||
|
||||
async openTerminalPanel(_request: proto.host.OpenTerminalRequest): Promise<proto.host.OpenTerminalResponse> {
|
||||
// Next phase: Send ACP extension notification or use createTerminal capability.
|
||||
// This would open/show the terminal panel in the editor.
|
||||
Logger.debug("[ACPWorkspaceServiceClient] openTerminalPanel called (stub)")
|
||||
if (this.debug) {
|
||||
console.error("[ACPWorkspaceServiceClient] openTerminalPanel called (stub)")
|
||||
}
|
||||
return proto.host.OpenTerminalResponse.create({})
|
||||
}
|
||||
|
||||
@@ -356,25 +436,23 @@ class ACPWorkspaceServiceClient implements WorkspaceServiceClientInterface {
|
||||
// This would execute the specified command in a terminal via the ACP client.
|
||||
// The ACP SDK provides createTerminal() which returns a TerminalHandle with
|
||||
// methods like currentOutput(), waitForExit(), kill(), and release().
|
||||
Logger.debug("[ACPWorkspaceServiceClient] executeCommandInTerminal called (stub)", {
|
||||
command: request.command,
|
||||
hasTerminalCapability: this._clientCapabilities?.terminal,
|
||||
})
|
||||
if (this.debug) {
|
||||
console.error("[ACPWorkspaceServiceClient] executeCommandInTerminal called (stub)", {
|
||||
command: request.command,
|
||||
hasTerminalCapability: this._clientCapabilities?.terminal,
|
||||
})
|
||||
}
|
||||
return proto.host.ExecuteCommandInTerminalResponse.create({})
|
||||
}
|
||||
|
||||
async openFolder(request: proto.host.OpenFolderRequest): Promise<proto.host.OpenFolderResponse> {
|
||||
// Next phase: Send ACP extension request to change workspace/folder.
|
||||
// This would open a new folder/workspace in the ACP client.
|
||||
Logger.debug("[ACPWorkspaceServiceClient] openFolder called (stub)", { path: request.path })
|
||||
if (this.debug) {
|
||||
console.error("[ACPWorkspaceServiceClient] openFolder called (stub)", { path: request.path })
|
||||
}
|
||||
return proto.host.OpenFolderResponse.create({ success: true })
|
||||
}
|
||||
|
||||
async searchWorkspaceItems(
|
||||
_request: proto.host.SearchWorkspaceItemsRequest,
|
||||
): Promise<proto.host.SearchWorkspaceItemsResponse> {
|
||||
throw new Error("searchWorkspaceItems is not implemented on the ACP host")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -401,14 +479,22 @@ export class ACPHostBridgeClientProvider implements HostBridgeClientProvider {
|
||||
* @param version - Version string for getHostVersion (optional)
|
||||
*/
|
||||
constructor(
|
||||
connection: acp.AgentSideConnection,
|
||||
clientCapabilities: acp.ClientCapabilities | undefined,
|
||||
sessionIdResolver: SessionIdResolver,
|
||||
cwdResolver: CwdResolver,
|
||||
version: string,
|
||||
debug: boolean = false,
|
||||
version: string = "1.0.0",
|
||||
) {
|
||||
this.workspaceClient = new ACPWorkspaceServiceClient(clientCapabilities, sessionIdResolver, cwdResolver)
|
||||
this.envClient = new ACPEnvServiceClient(clientCapabilities, sessionIdResolver, version)
|
||||
this.windowClient = new ACPWindowServiceClient(clientCapabilities, sessionIdResolver)
|
||||
this.diffClient = new ACPDiffServiceClient()
|
||||
this.workspaceClient = new ACPWorkspaceServiceClient(
|
||||
connection,
|
||||
clientCapabilities,
|
||||
sessionIdResolver,
|
||||
cwdResolver,
|
||||
debug,
|
||||
)
|
||||
this.envClient = new ACPEnvServiceClient(connection, clientCapabilities, sessionIdResolver, debug, version)
|
||||
this.windowClient = new ACPWindowServiceClient(connection, clientCapabilities, sessionIdResolver, debug)
|
||||
this.diffClient = new ACPDiffServiceClient(connection, clientCapabilities, sessionIdResolver, debug)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
/**
|
||||
* ClineAgent - Decoupled ACP Agent implementation for Cline CLI.
|
||||
* ACP Agent implementation for Cline CLI.
|
||||
*
|
||||
* This class implements the ACP (Agent Client Protocol) Agent interface,
|
||||
* allowing Cline to be used programmatically without stdio dependency.
|
||||
* It uses a callback pattern for permission requests and EventEmitters
|
||||
* for session updates, enabling embedding in other Node.js applications.
|
||||
*
|
||||
* For stdio-based ACP communication, use the AcpAgent wrapper class.
|
||||
* allowing Cline to be used as a subprocess agent by editors like Zed and JetBrains.
|
||||
*
|
||||
* @module acp
|
||||
*/
|
||||
@@ -28,8 +24,6 @@ import {
|
||||
groqModels,
|
||||
mistralDefaultModelId,
|
||||
mistralModels,
|
||||
moonshotDefaultModelId,
|
||||
moonshotModels,
|
||||
openAiCodexDefaultModelId,
|
||||
openAiNativeDefaultModelId,
|
||||
openAiNativeModels,
|
||||
@@ -38,36 +32,31 @@ 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"
|
||||
import { ExternalWebviewProvider } from "@/hosts/external/ExternalWebviewProvider.js"
|
||||
import { HostProvider } from "@/hosts/host-provider.js"
|
||||
import { FileEditProvider } from "@/integrations/editor/FileEditProvider"
|
||||
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
|
||||
import { StandaloneTerminalManager } from "@/integrations/terminal/index.js"
|
||||
import { AuthService } from "@/services/auth/AuthService.js"
|
||||
import { Logger } from "@/shared/services/Logger.js"
|
||||
import { secretStorage } from "@/shared/storage/ClineSecretStorage"
|
||||
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 { ACPDiffViewProvider } from "./ACPDiffViewProvider.js"
|
||||
import { ACPHostBridgeClientProvider } from "./ACPHostBridgeClientProvider.js"
|
||||
import { AcpTerminalManager } from "./AcpTerminalManager.js"
|
||||
import { translateMessage } from "./messageTranslator.js"
|
||||
import { handlePermissionResponse } from "./permissionHandler.js"
|
||||
import type { ClineAcpSession, ClineAgentOptions, PermissionHandler } from "./public-types.js"
|
||||
import { AcpSessionStatus } from "./public-types.js"
|
||||
import { type AcpSessionState } from "./types.js"
|
||||
import type { AcpAgentOptions, AcpSessionState, ClineAcpSession } 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 }> = {
|
||||
@@ -77,7 +66,6 @@ const providerModels: Record<string, { models: Record<string, unknown>; defaultI
|
||||
bedrock: { models: bedrockModels, defaultId: bedrockDefaultModelId },
|
||||
deepseek: { models: deepSeekModels, defaultId: deepSeekDefaultModelId },
|
||||
mistral: { models: mistralModels, defaultId: mistralDefaultModelId },
|
||||
moonshot: { models: moonshotModels, defaultId: moonshotDefaultModelId },
|
||||
groq: { models: groqModels, defaultId: groqDefaultModelId },
|
||||
xai: { models: xaiModels, defaultId: xaiDefaultModelId },
|
||||
}
|
||||
@@ -95,38 +83,24 @@ function getModelList(provider: string): string[] {
|
||||
* Cline's implementation of the ACP Agent interface.
|
||||
*
|
||||
* This agent bridges the ACP protocol with Cline's core Controller,
|
||||
* translating ACP requests into Controller operations and emitting
|
||||
* session updates via EventEmitters.
|
||||
*
|
||||
* This class is decoupled from the stdio connection, enabling:
|
||||
* - Programmatic usage without stdio dependency
|
||||
* - Running multiple concurrent sessions
|
||||
* - Handling ACP events via EventEmitter pattern
|
||||
*
|
||||
* For stdio-based ACP communication, use the AcpAgent wrapper class.
|
||||
* translating ACP requests into Controller operations and sending
|
||||
* session updates back to the client.
|
||||
*/
|
||||
export class ClineAgent implements acp.Agent {
|
||||
private readonly options: ClineAgentOptions
|
||||
export class AcpAgent implements acp.Agent {
|
||||
private readonly connection: acp.AgentSideConnection
|
||||
private readonly options: AcpAgentOptions
|
||||
private readonly ctx: CliContextResult
|
||||
|
||||
/** 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>()
|
||||
readonly sessions: Map<string, ClineAcpSession> = new Map()
|
||||
|
||||
/** Runtime state for active sessions */
|
||||
private readonly sessionStates: Map<string, AcpSessionState> = new Map()
|
||||
|
||||
/** Per-session event emitters for session updates */
|
||||
private readonly sessionEmitters: Map<string, ClineSessionEmitter> = new Map()
|
||||
|
||||
/** Permission handler callback for requesting user permission */
|
||||
private permissionHandler?: PermissionHandler
|
||||
|
||||
/** Client capabilities received during initialization */
|
||||
private clientCapabilities?: acp.ClientCapabilities
|
||||
|
||||
/** Auto-approval tracker for remembering "always allow" decisions */
|
||||
// private readonly autoApprovalTracker: AutoApprovalTracker = new AutoApprovalTracker()
|
||||
|
||||
/** Track last sent content for partial messages to compute deltas */
|
||||
private partialMessageLastContent: Map<number, string> = new Map()
|
||||
|
||||
@@ -139,41 +113,10 @@ export class ClineAgent implements acp.Agent {
|
||||
/** Shared WebviewProvider instance for auth and other operations */
|
||||
private webviewProvider: ReturnType<typeof HostProvider.get.prototype.createWebviewProvider> | undefined
|
||||
|
||||
constructor(options: ClineAgentOptions) {
|
||||
constructor(connection: acp.AgentSideConnection, options: AcpAgentOptions) {
|
||||
this.connection = connection
|
||||
this.options = options
|
||||
setRuntimeHooksDir(options.hooksDir)
|
||||
this.ctx = initializeCliContext({ clineDir: options.clineDir })
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the permission handler callback.
|
||||
*
|
||||
* This handler is called when the agent needs permission for a tool call.
|
||||
* The handler should present the request to the user and call the resolve
|
||||
* callback with their response.
|
||||
*
|
||||
* @param handler - The permission handler callback
|
||||
*/
|
||||
setPermissionHandler(handler: PermissionHandler): void {
|
||||
this.permissionHandler = handler
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the event emitter for a session.
|
||||
*
|
||||
* Use this to subscribe to session events like agent_message_chunk,
|
||||
* tool_call, etc.
|
||||
*
|
||||
* @param sessionId - The session ID
|
||||
* @returns The session's event emitter
|
||||
*/
|
||||
emitterForSession(sessionId: string): ClineSessionEmitter {
|
||||
let emitter = this.sessionEmitters.get(sessionId)
|
||||
if (!emitter) {
|
||||
emitter = new ClineSessionEmitter()
|
||||
this.sessionEmitters.set(sessionId, emitter)
|
||||
}
|
||||
return emitter
|
||||
this.ctx = initializeCliContext()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -182,11 +125,11 @@ export class ClineAgent implements acp.Agent {
|
||||
* This is the first method called by the client after establishing
|
||||
* the connection. The agent returns its protocol version and capabilities.
|
||||
*/
|
||||
async initialize(params: acp.InitializeRequest, connection?: acp.AgentSideConnection): Promise<acp.InitializeResponse> {
|
||||
async initialize(params: acp.InitializeRequest): Promise<acp.InitializeResponse> {
|
||||
this.clientCapabilities = params.clientCapabilities
|
||||
this.initializeHostProvider(this.clientCapabilities, connection)
|
||||
await ClineEndpoint.initialize(this.ctx.EXTENSION_DIR)
|
||||
await StateManager.initialize(this.ctx.storageContext)
|
||||
this.initializeHostProvider(this.clientCapabilities)
|
||||
await ClineEndpoint.initialize()
|
||||
await StateManager.initialize(this.ctx.extensionContext)
|
||||
|
||||
return {
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
@@ -204,7 +147,7 @@ export class ClineAgent implements acp.Agent {
|
||||
},
|
||||
agentInfo: {
|
||||
name: "cline",
|
||||
version: AGENT_VERSION,
|
||||
version: this.options.version,
|
||||
},
|
||||
authMethods: [
|
||||
{
|
||||
@@ -221,45 +164,43 @@ export class ClineAgent implements acp.Agent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the host provider with optional connection for ACP mode.
|
||||
*
|
||||
* When used with the AcpAgent wrapper, a connection is provided for
|
||||
* host bridge operations. When used programmatically, connection is
|
||||
* undefined and standalone providers are used.
|
||||
*
|
||||
* @param clientCapabilities - Client capabilities from initialization
|
||||
* @param connection - Optional ACP connection for host bridge operations
|
||||
*/
|
||||
initializeHostProvider(clientCapabilities?: acp.ClientCapabilities, connection?: acp.AgentSideConnection): void {
|
||||
initializeHostProvider(clientCapabilities?: acp.ClientCapabilities): void {
|
||||
const hostBridgeClientProvider = new ACPHostBridgeClientProvider(
|
||||
this.connection,
|
||||
clientCapabilities,
|
||||
() => this.currentActiveSessionId,
|
||||
() => this.sessions.get(this.currentActiveSessionId ?? "")?.cwd ?? process.cwd(),
|
||||
AGENT_VERSION,
|
||||
this.options.debug,
|
||||
this.options.version,
|
||||
)
|
||||
|
||||
HostProvider.initialize(
|
||||
() => new ExternalWebviewProvider(this.ctx.extensionContext),
|
||||
() => {
|
||||
if (clientCapabilities?.fs && connection) {
|
||||
return new ACPDiffViewProvider(connection, clientCapabilities, () => this.currentActiveSessionId)
|
||||
}
|
||||
// Fallback for programmatic use
|
||||
return new FileEditProvider()
|
||||
return new ACPDiffViewProvider(
|
||||
this.connection,
|
||||
clientCapabilities,
|
||||
() => this.currentActiveSessionId,
|
||||
this.options.debug,
|
||||
)
|
||||
},
|
||||
() => new ExternalCommentReviewController(),
|
||||
() => {
|
||||
if (clientCapabilities?.terminal && connection) {
|
||||
return new AcpTerminalManager(connection, clientCapabilities, () => this.currentActiveSessionId)
|
||||
if (clientCapabilities?.terminal) {
|
||||
return new AcpTerminalManager(
|
||||
this.connection,
|
||||
clientCapabilities,
|
||||
() => this.currentActiveSessionId,
|
||||
this.options.debug,
|
||||
)
|
||||
} else {
|
||||
return new StandaloneTerminalManager()
|
||||
}
|
||||
// Fallback for programmatic use
|
||||
return new StandaloneTerminalManager()
|
||||
},
|
||||
hostBridgeClientProvider,
|
||||
(message: string) => Logger.info(message),
|
||||
async (path: string) => {
|
||||
return AuthHandler.getInstance().getCallbackUrl(path)
|
||||
async () => {
|
||||
return AuthHandler.getInstance().getCallbackUrl()
|
||||
},
|
||||
async () => "", // get binary location not needed in ACP mode
|
||||
this.ctx.EXTENSION_DIR,
|
||||
@@ -275,14 +216,14 @@ export class ClineAgent implements acp.Agent {
|
||||
*/
|
||||
async newSession(params: acp.NewSessionRequest): Promise<acp.NewSessionResponse> {
|
||||
// Check if authentication is required
|
||||
const isAuthenticated = await isAuthConfigured()
|
||||
const isAuthenticated = await this.isAuthConfigured()
|
||||
if (!isAuthenticated) {
|
||||
throw RequestError.authRequired()
|
||||
}
|
||||
|
||||
const sessionId = crypto.randomUUID()
|
||||
|
||||
Logger.debug("[ClineAgent] newSession called:", {
|
||||
Logger.debug("[AcpAgent] newSession called:", {
|
||||
sessionId,
|
||||
cwd: params.cwd,
|
||||
mcpServers: params.mcpServers?.length ?? 0,
|
||||
@@ -299,25 +240,25 @@ 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,
|
||||
status: AcpSessionStatus.Idle,
|
||||
isProcessing: false,
|
||||
cancelled: false,
|
||||
pendingToolCalls: new Map(),
|
||||
}
|
||||
|
||||
this.sessionStates.set(sessionId, sessionState)
|
||||
|
||||
// Send available slash commands to the client
|
||||
// This is done asynchronously after session creation
|
||||
// This is done asynchronously after session creation per ACP spec
|
||||
await this.sendAvailableCommands(sessionId, controller).catch((error) => {
|
||||
Logger.debug("[ClineAgent] Failed to send available commands:", error)
|
||||
Logger.debug("[AcpAgent] Failed to send available commands:", error)
|
||||
})
|
||||
|
||||
// Get current model configuration for the response
|
||||
@@ -345,15 +286,14 @@ export class ClineAgent implements acp.Agent {
|
||||
|
||||
// Get current provider and model for the mode
|
||||
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
const currentProvider = stateManager.getGlobalSettingsKey(providerKey) as ApiProvider | undefined
|
||||
const modelKey = mode === "act" ? "actModeApiModelId" : "planModeApiModelId"
|
||||
|
||||
// Use provider-specific model ID key (e.g., cline uses actModeOpenRouterModelId)
|
||||
const modelKey = currentProvider ? getProviderModelIdKey(currentProvider, mode) : null
|
||||
const currentModelId = modelKey ? stateManager.getGlobalSettingsKey(modelKey) : undefined
|
||||
const currentProvider = stateManager.getGlobalSettingsKey(providerKey) as ApiProvider | undefined
|
||||
const currentModelId = stateManager.getGlobalSettingsKey(modelKey) as string | undefined
|
||||
|
||||
// Build the current model ID in provider/model format
|
||||
const currentFullModelId =
|
||||
currentProvider && currentModelId ? `${currentProvider}/${currentModelId}` : currentProvider || ""
|
||||
currentProvider && currentModelId ? `${currentProvider}/${currentModelId}` : currentProvider || "anthropic"
|
||||
|
||||
// Get available models based on provider
|
||||
let modelIds: string[] = []
|
||||
@@ -395,7 +335,7 @@ export class ClineAgent implements acp.Agent {
|
||||
throw new Error(`Session not found: ${params.sessionId}`)
|
||||
}
|
||||
|
||||
Logger.debug("[ClineAgent] unstable_setSessionModel called:", {
|
||||
Logger.debug("[AcpAgent] unstable_setSessionModel called:", {
|
||||
sessionId: params.sessionId,
|
||||
modelId: params.modelId,
|
||||
})
|
||||
@@ -411,11 +351,13 @@ export class ClineAgent implements acp.Agent {
|
||||
|
||||
const stateManager = StateManager.get()
|
||||
|
||||
// Update provider for both modes
|
||||
// Update model for both plan and act modes (use the same model for both)
|
||||
stateManager.setGlobalState("actModeApiProvider", provider)
|
||||
stateManager.setGlobalState("actModeApiModelId", modelId)
|
||||
stateManager.setGlobalState("planModeApiProvider", provider)
|
||||
stateManager.setGlobalState("planModeApiModelId", modelId)
|
||||
|
||||
// Update model ID using provider-specific keys (e.g., cline uses actModeOpenRouterModelId)
|
||||
// Also update the provider-specific model ID keys for both modes
|
||||
const actProviderModelKey = getProviderModelIdKey(provider, "act")
|
||||
if (actProviderModelKey) {
|
||||
stateManager.setGlobalState(actProviderModelKey, modelId)
|
||||
@@ -445,11 +387,11 @@ export class ClineAgent implements acp.Agent {
|
||||
*
|
||||
* The prompt flow:
|
||||
* 1. Extract content from the ACP prompt (text, images, files)
|
||||
* 2. Set up internal cline state subsription
|
||||
* 3. Initialize or continue cline task
|
||||
* 2. Set up state broadcasting (subscribe to controller updates)
|
||||
* 3. Initialize or continue task with Controller
|
||||
* 4. Translate ClineMessages to ACP SessionUpdates
|
||||
* 5. Handle permission requests for tools/commands
|
||||
* 6. Return when cline task completes, is cancelled, or needs user input
|
||||
* 6. Return when task completes, is cancelled, or needs user input
|
||||
*/
|
||||
async prompt(params: acp.PromptRequest): Promise<acp.PromptResponse> {
|
||||
const session = this.sessions.get(params.sessionId)
|
||||
@@ -459,22 +401,23 @@ export class ClineAgent implements acp.Agent {
|
||||
throw new Error(`Session not found: ${params.sessionId}`)
|
||||
}
|
||||
|
||||
if (sessionState.status === AcpSessionStatus.Processing) {
|
||||
if (sessionState.isProcessing) {
|
||||
throw new Error(`Session ${params.sessionId} is already processing a prompt`)
|
||||
}
|
||||
|
||||
const controller = this.#sessionControllers.get(session)
|
||||
const controller = session.controller
|
||||
if (!controller) {
|
||||
throw new Error("Controller not initialized for session. This is a bug in the ACP agent setup.")
|
||||
}
|
||||
|
||||
Logger.debug("[ClineAgent] prompt called:", {
|
||||
Logger.debug("[AcpAgent] prompt called:", {
|
||||
sessionId: params.sessionId,
|
||||
promptLength: params.prompt.length,
|
||||
})
|
||||
|
||||
// Mark session as processing and set as current active session
|
||||
sessionState.status = AcpSessionStatus.Processing
|
||||
sessionState.isProcessing = true
|
||||
sessionState.cancelled = false
|
||||
session.lastActivityAt = Date.now()
|
||||
this.currentActiveSessionId = params.sessionId
|
||||
|
||||
@@ -519,7 +462,7 @@ export class ClineAgent implements acp.Agent {
|
||||
|
||||
if (isLoadedSession && !hasActiveTask) {
|
||||
// First prompt on a loaded session - resume the task from history
|
||||
Logger.debug("[ClineAgent] Resuming loaded session:", params.sessionId)
|
||||
Logger.debug("[AcpAgent] Resuming loaded session:", params.sessionId)
|
||||
|
||||
// Clear the flag so subsequent prompts are handled normally
|
||||
session.isLoadedFromHistory = false
|
||||
@@ -534,7 +477,7 @@ export class ClineAgent implements acp.Agent {
|
||||
}
|
||||
} else if (hasActiveTask && controller.task) {
|
||||
// Continue existing task - respond to pending ask
|
||||
Logger.debug("[ClineAgent] Continuing existing task:", controller.task.taskId)
|
||||
Logger.debug("[AcpAgent] Continuing existing task:", controller.task.taskId)
|
||||
|
||||
// Find the last ask message and respond to it
|
||||
const messages = controller.task.messageStateHandler.getClineMessages()
|
||||
@@ -545,12 +488,12 @@ export class ClineAgent implements acp.Agent {
|
||||
} else {
|
||||
// No pending ask - treat as new user message
|
||||
// This shouldn't normally happen but handle gracefully
|
||||
Logger.debug("[ClineAgent] No pending ask found, starting new task")
|
||||
Logger.debug("[AcpAgent] No pending ask found, starting new task")
|
||||
await controller.initTask(textContent, imageContent, fileResources)
|
||||
}
|
||||
} else {
|
||||
// Start new task
|
||||
Logger.debug("[ClineAgent] Starting new task")
|
||||
Logger.debug("[AcpAgent] Starting new task")
|
||||
await controller.initTask(textContent, imageContent, fileResources)
|
||||
}
|
||||
|
||||
@@ -559,7 +502,7 @@ export class ClineAgent implements acp.Agent {
|
||||
const onClineMessagesChanged = (change: ClineMessageChange) => {
|
||||
this.handleClineMessagesChanged(params.sessionId, sessionState, change, resolvePrompt, promptResolved).catch(
|
||||
(error) => {
|
||||
Logger.debug("[ClineAgent] Error handling clineMessagesChanged:", error)
|
||||
Logger.debug("[AcpAgent] Error handling clineMessagesChanged:", error)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -576,7 +519,7 @@ export class ClineAgent implements acp.Agent {
|
||||
if (!promptResolved.value) {
|
||||
promptResolved.value = true
|
||||
// Send error as session update before returning
|
||||
await this.emitSessionUpdate(params.sessionId, {
|
||||
await this.sendSessionUpdate(params.sessionId, {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: {
|
||||
type: "text",
|
||||
@@ -592,10 +535,10 @@ export class ClineAgent implements acp.Agent {
|
||||
try {
|
||||
cleanup()
|
||||
} catch (error) {
|
||||
Logger.debug("[ClineAgent] Error during cleanup:", error)
|
||||
Logger.debug("[AcpAgent] Error during cleanup:", error)
|
||||
}
|
||||
}
|
||||
sessionState.status = AcpSessionStatus.Idle
|
||||
sessionState.isProcessing = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -606,7 +549,7 @@ export class ClineAgent implements acp.Agent {
|
||||
resolvePrompt: (response: acp.PromptResponse) => void,
|
||||
promptResolved: { value: boolean },
|
||||
): Promise<void> {
|
||||
Logger.debug("[ClineAgent] handleClineMessagesChanged:", change)
|
||||
Logger.debug("[AcpAgent] handleClineMessagesChanged:", change)
|
||||
try {
|
||||
switch (change.type) {
|
||||
case "add":
|
||||
@@ -633,7 +576,7 @@ export class ClineAgent implements acp.Agent {
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.debug("[ClineAgent] Error handling clineMessagesChanged:", error)
|
||||
Logger.debug("[AcpAgent] Error handling clineMessagesChanged:", error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,16 +600,10 @@ export class ClineAgent implements acp.Agent {
|
||||
permissionRequest: Omit<acp.RequestPermissionRequest, "sessionId">,
|
||||
): Promise<void> {
|
||||
const session = this.sessions.get(sessionId)
|
||||
|
||||
if (!session) {
|
||||
Logger.debug("[ClineAgent] No session found for permission request")
|
||||
return
|
||||
}
|
||||
|
||||
const controller = this.#sessionControllers.get(session)
|
||||
const controller = session?.controller
|
||||
|
||||
if (!controller?.task) {
|
||||
Logger.debug("[ClineAgent] No active task for permission request")
|
||||
Logger.debug("[AcpAgent] No active task for permission request")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -676,7 +613,7 @@ export class ClineAgent implements acp.Agent {
|
||||
// Request permission from the client
|
||||
const response = await this.requestPermission(sessionId, permissionRequest.toolCall, permissionRequest.options)
|
||||
|
||||
Logger.debug("[ClineAgent] Permission response received:", response.outcome)
|
||||
Logger.debug("[AcpAgent] Permission response received:", response.outcome)
|
||||
|
||||
// Handle the response
|
||||
const result = handlePermissionResponse(response, askType)
|
||||
@@ -684,14 +621,14 @@ export class ClineAgent implements acp.Agent {
|
||||
// Update tool call status based on permission result
|
||||
if (sessionState.currentToolCallId) {
|
||||
if (result.cancelled) {
|
||||
await this.emitSessionUpdate(sessionId, {
|
||||
await this.sendSessionUpdate(sessionId, {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: sessionState.currentToolCallId,
|
||||
status: "failed",
|
||||
rawOutput: { reason: "cancelled" },
|
||||
})
|
||||
} else if (result.response === "noButtonClicked") {
|
||||
await this.emitSessionUpdate(sessionId, {
|
||||
await this.sendSessionUpdate(sessionId, {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: sessionState.currentToolCallId,
|
||||
status: "failed",
|
||||
@@ -699,7 +636,7 @@ export class ClineAgent implements acp.Agent {
|
||||
})
|
||||
} else {
|
||||
// Permission granted - mark as in_progress
|
||||
await this.emitSessionUpdate(sessionId, {
|
||||
await this.sendSessionUpdate(sessionId, {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: sessionState.currentToolCallId,
|
||||
status: "in_progress",
|
||||
@@ -716,11 +653,11 @@ export class ClineAgent implements acp.Agent {
|
||||
await controller.task.handleWebviewAskResponse(result.response, result.text)
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.debug("[ClineAgent] Error handling permission request:", error)
|
||||
Logger.debug("[AcpAgent] Error handling permission request:", error)
|
||||
|
||||
// Update tool call status to failed
|
||||
if (sessionState.currentToolCallId) {
|
||||
await this.emitSessionUpdate(sessionId, {
|
||||
await this.sendSessionUpdate(sessionId, {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: sessionState.currentToolCallId,
|
||||
status: "failed",
|
||||
@@ -794,13 +731,14 @@ export class ClineAgent implements acp.Agent {
|
||||
const lastText = this.partialMessageLastContent.get(messageKey) || ""
|
||||
|
||||
// Determine if this is a text-streaming message type that needs delta handling
|
||||
// Note: act_mode_respond is NOT included here because its text content was already
|
||||
// sent via the say: "text" message. Including it would cause duplicate output.
|
||||
const isTextStreamingMessage =
|
||||
(message.type === "say" &&
|
||||
(message.say === "text" || message.say === "reasoning" || message.say === "completion_result")) ||
|
||||
(message.type === "ask" &&
|
||||
(message.ask === "followup" || message.ask === "plan_mode_respond" || message.ask === "completion_result"))
|
||||
(message.ask === "followup" ||
|
||||
message.ask === "plan_mode_respond" ||
|
||||
message.ask === "act_mode_respond" ||
|
||||
message.ask === "completion_result"))
|
||||
|
||||
if (isTextStreamingMessage && message.text) {
|
||||
// Extract the actual text content for JSON-wrapped messages
|
||||
@@ -835,16 +773,9 @@ export class ClineAgent implements acp.Agent {
|
||||
const sessionUpdate: "agent_message_chunk" | "agent_thought_chunk" =
|
||||
message.type === "say" && message.say === "reasoning" ? "agent_thought_chunk" : "agent_message_chunk"
|
||||
|
||||
// For completion_result messages, add a leading newline to separate from previous content
|
||||
// This ensures the completion message appears on a new line after any preceding text
|
||||
const isCompletionResult =
|
||||
(message.type === "say" && message.say === "completion_result") ||
|
||||
(message.type === "ask" && message.ask === "completion_result")
|
||||
const needsNewline = isCompletionResult && lastText === ""
|
||||
|
||||
await this.emitSessionUpdate(sessionId, {
|
||||
await this.sendSessionUpdate(sessionId, {
|
||||
sessionUpdate,
|
||||
content: { type: "text", text: needsNewline ? `\n${textDelta}` : textDelta },
|
||||
content: { type: "text", text: textDelta },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -861,7 +792,7 @@ export class ClineAgent implements acp.Agent {
|
||||
|
||||
// Send all updates produced by the translator
|
||||
for (const update of result.updates) {
|
||||
await this.emitSessionUpdate(sessionId, update)
|
||||
await this.sendSessionUpdate(sessionId, update)
|
||||
}
|
||||
|
||||
// Track the toolCallId for this message so subsequent updates reuse it
|
||||
@@ -897,27 +828,23 @@ 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:", {
|
||||
Logger.debug("[AcpAgent] cancel called:", {
|
||||
sessionId: params.sessionId,
|
||||
status: sessionState?.status,
|
||||
isProcessing: sessionState?.isProcessing,
|
||||
})
|
||||
|
||||
if (sessionState) {
|
||||
sessionState.status = AcpSessionStatus.Cancelled
|
||||
sessionState.cancelled = true
|
||||
|
||||
// If we have an active controller task, cancel it
|
||||
const controller = this.#sessionControllers.get(session)
|
||||
const controller = session?.controller
|
||||
if (controller?.task) {
|
||||
try {
|
||||
await controller.cancelTask()
|
||||
} catch (error) {
|
||||
Logger.debug("[ClineAgent] Error cancelling task:", error)
|
||||
Logger.debug("[AcpAgent] Error cancelling task:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -937,7 +864,7 @@ export class ClineAgent implements acp.Agent {
|
||||
throw new Error(`Session not found: ${params.sessionId}`)
|
||||
}
|
||||
|
||||
Logger.debug("[ClineAgent] setSessionMode called:", {
|
||||
Logger.debug("[AcpAgent] setSessionMode called:", {
|
||||
sessionId: params.sessionId,
|
||||
modeId: params.modeId,
|
||||
})
|
||||
@@ -953,7 +880,7 @@ export class ClineAgent implements acp.Agent {
|
||||
session.lastActivityAt = Date.now()
|
||||
|
||||
// Update Controller mode if active
|
||||
const controller = this.#sessionControllers.get(session)
|
||||
const controller = session.controller
|
||||
if (controller) {
|
||||
controller.stateManager.setGlobalState("mode", session.mode)
|
||||
|
||||
@@ -974,7 +901,7 @@ export class ClineAgent implements acp.Agent {
|
||||
* - OpenAI Codex/ChatGPT authentication (openai-codex-oauth)
|
||||
*/
|
||||
async authenticate(params: acp.AuthenticateRequest): Promise<acp.AuthenticateResponse> {
|
||||
Logger.debug("[ClineAgent] authenticate called:", { methodId: params.methodId })
|
||||
Logger.debug("[AcpAgent] authenticate called:", { methodId: params.methodId })
|
||||
|
||||
// Handle OpenAI Codex OAuth flow
|
||||
if (params.methodId === "openai-codex-oauth") {
|
||||
@@ -989,15 +916,15 @@ export class ClineAgent implements acp.Agent {
|
||||
const authHandler = AuthHandler.getInstance()
|
||||
authHandler.setEnabled(true)
|
||||
|
||||
Logger.debug("[ClineAgent] AuthHandler enabled, getting callback URL...")
|
||||
Logger.debug("[AcpAgent] AuthHandler enabled, getting callback URL...")
|
||||
|
||||
// Get the callback URL first to ensure the server is ready
|
||||
let callbackUrl: string
|
||||
try {
|
||||
callbackUrl = await authHandler.getCallbackUrl("/auth")
|
||||
Logger.debug("[ClineAgent] Callback URL ready:", callbackUrl)
|
||||
callbackUrl = await authHandler.getCallbackUrl()
|
||||
Logger.debug("[AcpAgent] Callback URL ready:", callbackUrl)
|
||||
} catch (error) {
|
||||
Logger.error("[ClineAgent] Failed to get callback URL:", error)
|
||||
Logger.error("[AcpAgent] Failed to get callback URL:", error)
|
||||
throw new Error(`Failed to start auth server: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
|
||||
@@ -1011,12 +938,12 @@ export class ClineAgent implements acp.Agent {
|
||||
// Get the AuthService instance with the webview's controller
|
||||
const authService = AuthService.getInstance(this.webviewProvider.controller)
|
||||
|
||||
Logger.debug("[ClineAgent] Starting OAuth flow...")
|
||||
Logger.debug("[AcpAgent] Starting OAuth flow...")
|
||||
|
||||
// Start the OAuth flow - this opens the browser
|
||||
await authService.createAuthRequest()
|
||||
|
||||
Logger.debug("[ClineAgent] Browser opened, waiting for callback...")
|
||||
Logger.debug("[AcpAgent] Browser opened, waiting for callback...")
|
||||
|
||||
// Wait for authentication to complete (with timeout)
|
||||
const AUTH_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes
|
||||
@@ -1025,14 +952,13 @@ export class ClineAgent implements acp.Agent {
|
||||
const startTime = Date.now()
|
||||
|
||||
while (Date.now() - startTime < AUTH_TIMEOUT_MS) {
|
||||
const stateManager = StateManager.get()
|
||||
|
||||
// Check if auth data has been stored
|
||||
const authData = stateManager.getSecretKey("cline:clineAccountId")
|
||||
const authData = await secretStorage.get("cline:clineAccountId")
|
||||
if (authData) {
|
||||
Logger.debug("[ClineAgent] Authentication successful")
|
||||
Logger.debug("[AcpAgent] Authentication successful")
|
||||
|
||||
// Set up the provider configuration for cline
|
||||
const stateManager = StateManager.get()
|
||||
stateManager.setGlobalState("actModeApiProvider", "cline")
|
||||
stateManager.setGlobalState("planModeApiProvider", "cline")
|
||||
await stateManager.flushPendingState()
|
||||
@@ -1046,7 +972,7 @@ export class ClineAgent implements acp.Agent {
|
||||
|
||||
throw new Error("Authentication timed out. Please try again.")
|
||||
} catch (error) {
|
||||
Logger.error("[ClineAgent] Authentication error:", error)
|
||||
Logger.error("[AcpAgent] Authentication error:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1056,58 +982,53 @@ export class ClineAgent implements acp.Agent {
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Emit a session update to the client.
|
||||
*
|
||||
* This method emits the update via the session's event emitter.
|
||||
* Consumers (e.g. AcpAgent) subscribe to these events
|
||||
* Send a session update to the client.
|
||||
*/
|
||||
private async emitSessionUpdate(sessionId: string, update: acp.SessionUpdate): Promise<void> {
|
||||
const emitter = this.emitterForSession(sessionId)
|
||||
|
||||
private async sendSessionUpdate(sessionId: string, update: acp.SessionUpdate): Promise<void> {
|
||||
try {
|
||||
emitter.emit(update.sessionUpdate, update)
|
||||
await this.connection.sessionUpdate({
|
||||
sessionId,
|
||||
update,
|
||||
})
|
||||
} catch (error) {
|
||||
Logger.debug("[ClineAgent] Error emitting session update:", error)
|
||||
emitter.emit("error", error instanceof Error ? error : new Error(String(error)))
|
||||
Logger.debug("[AcpAgent] Error sending session update:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request permission from the client for a tool call.
|
||||
*
|
||||
* This method uses the permission handler callback to request permission.
|
||||
* The handler is set by the AcpAgent wrapper or by programmatic users.
|
||||
* This method sends a `session/request_permission` request to the client
|
||||
* and returns the user's decision.
|
||||
*
|
||||
* @param sessionId - The session ID
|
||||
* @param toolCall - The tool call update containing details about the operation
|
||||
* @param options - Available permission options for the user to choose from
|
||||
* @returns The permission response from the client
|
||||
*/
|
||||
protected async requestPermission(
|
||||
async requestPermission(
|
||||
sessionId: string,
|
||||
toolCall: acp.ToolCallUpdate,
|
||||
options: acp.PermissionOption[],
|
||||
): Promise<acp.RequestPermissionResponse> {
|
||||
Logger.debug("[ClineAgent] Requesting permission:", {
|
||||
Logger.debug("[AcpAgent] Requesting permission:", {
|
||||
sessionId,
|
||||
toolCallId: toolCall.toolCallId,
|
||||
options: options.map((o) => o.optionId),
|
||||
})
|
||||
|
||||
if (!this.permissionHandler) {
|
||||
// No permission handler set - auto-reject for safety
|
||||
Logger.debug("[ClineAgent] No permission handler set, auto-rejecting")
|
||||
return { outcome: "rejected" as unknown as acp.RequestPermissionOutcome }
|
||||
}
|
||||
|
||||
return await this.permissionHandler({ sessionId, toolCall, options })
|
||||
return await this.connection.requestPermission({
|
||||
sessionId,
|
||||
toolCall,
|
||||
options,
|
||||
})
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
for (const [sessionId, session] of this.sessions) {
|
||||
const controller = this.#sessionControllers.get(session)
|
||||
await controller?.task?.abortTask()
|
||||
await controller?.stateManager.flushPendingState()
|
||||
await controller?.dispose()
|
||||
await session.controller?.task?.abortTask()
|
||||
await session.controller?.stateManager.flushPendingState()
|
||||
await session.controller?.dispose()
|
||||
this.sessions.delete(sessionId)
|
||||
this.sessionStates.delete(sessionId)
|
||||
}
|
||||
@@ -1148,21 +1069,63 @@ export class ClineAgent implements acp.Agent {
|
||||
}))
|
||||
|
||||
// Send the available_commands_update notification
|
||||
await this.emitSessionUpdate(sessionId, {
|
||||
await this.sendSessionUpdate(sessionId, {
|
||||
sessionUpdate: "available_commands_update",
|
||||
availableCommands,
|
||||
})
|
||||
|
||||
Logger.debug("[ClineAgent] Sent available commands:", {
|
||||
Logger.debug("[AcpAgent] Sent available commands:", {
|
||||
sessionId,
|
||||
commandCount: availableCommands.length,
|
||||
commands: availableCommands.map((c) => c.name),
|
||||
})
|
||||
} catch (error) {
|
||||
Logger.debug("[ClineAgent] Error sending available commands:", error)
|
||||
Logger.debug("[AcpAgent] Error sending available commands:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
const authData = await secretStorage.get("cline:clineAccountId")
|
||||
return !!authData
|
||||
}
|
||||
|
||||
// For OpenAI Codex provider, check OAuth credentials
|
||||
if (currentProvider === "openai-codex") {
|
||||
openAiCodexOAuthManager.initialize(this.ctx.extensionContext)
|
||||
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]
|
||||
for (const field of fields) {
|
||||
const value = await secretStorage.get(field)
|
||||
if (value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle OpenAI Codex OAuth authentication flow.
|
||||
*
|
||||
@@ -1173,13 +1136,16 @@ export class ClineAgent implements acp.Agent {
|
||||
* 4. Configures the provider on success
|
||||
*/
|
||||
private async authenticateOpenAiCodex(): Promise<acp.AuthenticateResponse> {
|
||||
Logger.debug("[ClineAgent] Starting OpenAI Codex OAuth flow...")
|
||||
Logger.debug("[AcpAgent] Starting OpenAI Codex OAuth flow...")
|
||||
|
||||
try {
|
||||
// Initialize the OAuth manager with extension context
|
||||
openAiCodexOAuthManager.initialize(this.ctx.extensionContext)
|
||||
|
||||
// Get the authorization URL and start the callback server
|
||||
const authUrl = openAiCodexOAuthManager.startAuthorizationFlow()
|
||||
|
||||
Logger.debug("[ClineAgent] Opening browser for OpenAI Codex auth:", authUrl)
|
||||
Logger.debug("[AcpAgent] Opening browser for OpenAI Codex auth:", authUrl)
|
||||
|
||||
// Open browser to authorization URL
|
||||
await openExternal(authUrl)
|
||||
@@ -1187,24 +1153,21 @@ export class ClineAgent implements acp.Agent {
|
||||
// Wait for the callback (blocks until auth completes or times out)
|
||||
await openAiCodexOAuthManager.waitForCallback()
|
||||
|
||||
Logger.debug("[ClineAgent] OpenAI Codex authentication successful")
|
||||
Logger.debug("[AcpAgent] OpenAI Codex authentication successful")
|
||||
|
||||
// Success - configure the provider
|
||||
const stateManager = StateManager.get()
|
||||
stateManager.setGlobalState("actModeApiProvider", "openai-codex")
|
||||
stateManager.setGlobalState("planModeApiProvider", "openai-codex")
|
||||
// Use provider-specific model ID keys for consistency
|
||||
const actModelKey = getProviderModelIdKey("openai-codex", "act")
|
||||
const planModelKey = getProviderModelIdKey("openai-codex", "plan")
|
||||
if (actModelKey) stateManager.setGlobalState(actModelKey, openAiCodexDefaultModelId)
|
||||
if (planModelKey) stateManager.setGlobalState(planModelKey, openAiCodexDefaultModelId)
|
||||
stateManager.setGlobalState("actModeApiModelId", openAiCodexDefaultModelId)
|
||||
stateManager.setGlobalState("planModeApiModelId", openAiCodexDefaultModelId)
|
||||
await stateManager.flushPendingState()
|
||||
|
||||
return {}
|
||||
} catch (error) {
|
||||
// Clean up on error
|
||||
openAiCodexOAuthManager.cancelAuthorizationFlow()
|
||||
Logger.error("[ClineAgent] OpenAI Codex authentication error:", error)
|
||||
Logger.error("[AcpAgent] OpenAI Codex authentication error:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,11 @@
|
||||
|
||||
import type * as acp from "@agentclientprotocol/sdk"
|
||||
import type { TerminalHandle } from "@agentclientprotocol/sdk"
|
||||
import { DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT, PROCESS_HOT_TIMEOUT_NORMAL } from "@integrations/terminal/constants"
|
||||
import {
|
||||
DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT,
|
||||
DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT,
|
||||
PROCESS_HOT_TIMEOUT_NORMAL,
|
||||
} from "@integrations/terminal/constants"
|
||||
import type {
|
||||
ITerminal,
|
||||
ITerminalManager,
|
||||
@@ -22,7 +26,6 @@ import type {
|
||||
TerminalProcessResultPromise,
|
||||
} from "@integrations/terminal/types"
|
||||
import { EventEmitter } from "events"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { SessionIdResolver } from "./ACPDiffViewProvider"
|
||||
|
||||
// =============================================================================
|
||||
@@ -138,14 +141,13 @@ export interface ManagedTerminal {
|
||||
* Wraps ACP terminal operations and emits events compatible with ITerminalProcess.
|
||||
*/
|
||||
class AcpTerminalProcess extends EventEmitter<TerminalProcessEvents> implements ITerminalProcess {
|
||||
isHot = false
|
||||
waitForShellIntegration = false
|
||||
isHot: boolean = false
|
||||
waitForShellIntegration: boolean = false
|
||||
|
||||
private _unretrievedOutput = ""
|
||||
private _continued = false
|
||||
private _completed = false
|
||||
private _unretrievedOutput: string = ""
|
||||
private _continued: boolean = false
|
||||
private _completed: boolean = false
|
||||
private _hotTimeout: NodeJS.Timeout | null = null
|
||||
private _exitWaitTimeout: NodeJS.Timeout | null = null
|
||||
private readonly manager: AcpTerminalManager
|
||||
private readonly terminalId: string
|
||||
private pollInterval: NodeJS.Timeout | null = null
|
||||
@@ -158,7 +160,7 @@ class AcpTerminalProcess extends EventEmitter<TerminalProcessEvents> implements
|
||||
|
||||
continue(): void {
|
||||
this._continued = true
|
||||
this.cleanup()
|
||||
this.stopPolling()
|
||||
this.emit("continue")
|
||||
}
|
||||
|
||||
@@ -169,24 +171,8 @@ class AcpTerminalProcess extends EventEmitter<TerminalProcessEvents> implements
|
||||
}
|
||||
|
||||
async terminate(): Promise<void> {
|
||||
this.cleanup()
|
||||
await this.manager.kill(this.terminalId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all timers and intervals.
|
||||
* Called on continue, terminate, or completion to prevent memory leaks.
|
||||
*/
|
||||
private cleanup(): void {
|
||||
this.stopPolling()
|
||||
if (this._hotTimeout) {
|
||||
clearTimeout(this._hotTimeout)
|
||||
this._hotTimeout = null
|
||||
}
|
||||
if (this._exitWaitTimeout) {
|
||||
clearTimeout(this._exitWaitTimeout)
|
||||
this._exitWaitTimeout = null
|
||||
}
|
||||
await this.manager.kill(this.terminalId)
|
||||
}
|
||||
|
||||
private stopPolling(): void {
|
||||
@@ -252,11 +238,10 @@ class AcpTerminalProcess extends EventEmitter<TerminalProcessEvents> implements
|
||||
this.manager.waitForExit(this.terminalId).then((result) => {
|
||||
if (result.success && !this._continued && !this._completed) {
|
||||
// The polling loop should handle this, but ensure we emit completed
|
||||
// Track the timeout so it can be cleaned up if the process is terminated early
|
||||
this._exitWaitTimeout = setTimeout(() => {
|
||||
setTimeout(() => {
|
||||
if (!this._continued && !this._completed) {
|
||||
this._completed = true
|
||||
this.cleanup()
|
||||
this.stopPolling()
|
||||
this.emit("completed")
|
||||
}
|
||||
}, 200)
|
||||
@@ -314,7 +299,7 @@ class AcpTerminal implements ITerminal {
|
||||
sendText(_text: string, _addNewLine?: boolean): void {
|
||||
// ACP terminals don't support interactive input this way
|
||||
// Commands are run via runCommand
|
||||
Logger.debug("[AcpTerminal] sendText not supported for ACP terminals")
|
||||
console.error("[AcpTerminal] sendText not supported for ACP terminals")
|
||||
}
|
||||
|
||||
show(): void {
|
||||
@@ -328,7 +313,7 @@ class AcpTerminal implements ITerminal {
|
||||
dispose(): void {
|
||||
// Release the terminal
|
||||
this._manager.release(this._managedTerminal.id).catch((err) => {
|
||||
Logger.debug("[AcpTerminal] Error releasing terminal:", err)
|
||||
console.error("[AcpTerminal] Error releasing terminal:", err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -385,6 +370,7 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
private readonly connection: acp.AgentSideConnection
|
||||
private readonly clientCapabilities: acp.ClientCapabilities | undefined
|
||||
private readonly sessionIdResolver: SessionIdResolver
|
||||
private readonly debug: boolean
|
||||
|
||||
/** Active terminals indexed by their string ID */
|
||||
private readonly terminals: Map<string, ManagedTerminal> = new Map()
|
||||
@@ -393,7 +379,7 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
private readonly numericIdToStringId: Map<number, string> = new Map()
|
||||
|
||||
/** Next numeric ID to assign */
|
||||
private nextNumericId = 1
|
||||
private nextNumericId: number = 1
|
||||
|
||||
/** Active processes indexed by numeric terminal ID */
|
||||
private readonly processes: Map<number, AcpTerminalProcess> = new Map()
|
||||
@@ -402,8 +388,11 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
private readonly terminalInfos: Map<number, TerminalInfo> = new Map()
|
||||
|
||||
// Configuration options for ITerminalManager
|
||||
private terminalReuseEnabled = true
|
||||
private terminalReuseEnabled: boolean = true
|
||||
private terminalOutputLineLimit: number = DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT
|
||||
private subagentTerminalOutputLineLimit: number = DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT
|
||||
private shellIntegrationTimeout: number = 0
|
||||
private defaultTerminalProfile: string = ""
|
||||
|
||||
/**
|
||||
* Creates a new AcpTerminalManager.
|
||||
@@ -411,15 +400,18 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
* @param connection - The ACP agent-side connection
|
||||
* @param clientCapabilities - The client's advertised capabilities
|
||||
* @param sessionIdResolver - The current session ID
|
||||
* @param debug - Whether to enable debug logging
|
||||
*/
|
||||
constructor(
|
||||
connection: acp.AgentSideConnection,
|
||||
clientCapabilities: acp.ClientCapabilities | undefined,
|
||||
sessionIdResolver: SessionIdResolver,
|
||||
debug: boolean = false,
|
||||
) {
|
||||
this.connection = connection
|
||||
this.clientCapabilities = clientCapabilities
|
||||
this.sessionIdResolver = sessionIdResolver
|
||||
this.debug = debug
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -575,7 +567,9 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
this.numericIdToStringId.set(numericId, managedTerminal.id)
|
||||
this.terminalInfos.set(numericId, terminalInfo)
|
||||
|
||||
Logger.debug("[AcpTerminalManager] Created terminal:", { numericId, cwd })
|
||||
if (this.debug) {
|
||||
console.error("[AcpTerminalManager] Created terminal:", { numericId, cwd })
|
||||
}
|
||||
|
||||
return terminalInfo
|
||||
}
|
||||
@@ -626,7 +620,7 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
disposeAll(): void {
|
||||
// Release all terminals
|
||||
this.releaseAll().catch((err) => {
|
||||
Logger.debug("[AcpTerminalManager] Error releasing terminals:", err)
|
||||
console.error("[AcpTerminalManager] Error releasing terminals:", err)
|
||||
})
|
||||
|
||||
// Clear all tracking
|
||||
@@ -635,15 +629,17 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
this.processes.clear()
|
||||
this.terminalInfos.clear()
|
||||
|
||||
Logger.debug("[AcpTerminalManager] disposeAll complete")
|
||||
if (this.debug) {
|
||||
console.error("[AcpTerminalManager] disposeAll complete")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the timeout for waiting for shell integration.
|
||||
* @param timeout Timeout in milliseconds
|
||||
*/
|
||||
setShellIntegrationTimeout(_timeout: number): void {
|
||||
// no-op
|
||||
setShellIntegrationTimeout(timeout: number): void {
|
||||
this.shellIntegrationTimeout = timeout
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -662,22 +658,35 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
this.terminalOutputLineLimit = limit
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum number of output lines for subagent commands.
|
||||
* @param limit Maximum number of lines
|
||||
*/
|
||||
setSubagentTerminalOutputLineLimit(limit: number): void {
|
||||
this.subagentTerminalOutputLineLimit = limit
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default terminal profile.
|
||||
* @param profile The profile identifier
|
||||
*/
|
||||
setDefaultTerminalProfile(_profile: string): void {
|
||||
// no-op
|
||||
setDefaultTerminalProfile(profile: string): void {
|
||||
this.defaultTerminalProfile = profile
|
||||
}
|
||||
|
||||
/**
|
||||
* Process output lines, potentially truncating if over limit.
|
||||
* @param outputLines Array of output lines
|
||||
* @param overrideLimit Optional limit override
|
||||
* @param isSubagentCommand Whether this is a subagent command
|
||||
* @returns Processed output string
|
||||
*/
|
||||
processOutput(outputLines: string[], overrideLimit?: number): string {
|
||||
const limit = overrideLimit !== undefined ? overrideLimit : this.terminalOutputLineLimit
|
||||
processOutput(outputLines: string[], overrideLimit?: number, isSubagentCommand?: boolean): string {
|
||||
const limit = isSubagentCommand
|
||||
? overrideLimit !== undefined
|
||||
? overrideLimit
|
||||
: this.subagentTerminalOutputLineLimit
|
||||
: this.terminalOutputLineLimit
|
||||
|
||||
if (outputLines.length > limit) {
|
||||
const halfLimit = Math.floor(limit / 2)
|
||||
@@ -710,7 +719,9 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
return { error: "Client does not support terminal capability" }
|
||||
}
|
||||
|
||||
Logger.debug("[AcpTerminalManager] createTerminal:", options)
|
||||
if (this.debug) {
|
||||
console.error("[AcpTerminalManager] createTerminal:", options)
|
||||
}
|
||||
|
||||
try {
|
||||
const request: acp.CreateTerminalRequest = {
|
||||
@@ -751,13 +762,17 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
this.terminals.set(handle.id, managedTerminal)
|
||||
this.numericIdToStringId.set(numericId, handle.id)
|
||||
|
||||
Logger.debug("[AcpTerminalManager] createTerminal success:", { id: handle.id, numericId })
|
||||
if (this.debug) {
|
||||
console.error("[AcpTerminalManager] createTerminal success:", { id: handle.id, numericId })
|
||||
}
|
||||
|
||||
return managedTerminal
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
Logger.debug("[AcpTerminalManager] createTerminal error:", errorMessage)
|
||||
if (this.debug) {
|
||||
console.error("[AcpTerminalManager] createTerminal error:", errorMessage)
|
||||
}
|
||||
|
||||
return { error: errorMessage }
|
||||
}
|
||||
@@ -790,7 +805,9 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
}
|
||||
}
|
||||
|
||||
Logger.debug("[AcpTerminalManager] getOutput:", { terminalId })
|
||||
if (this.debug) {
|
||||
console.error("[AcpTerminalManager] getOutput:", { terminalId })
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await terminal.handle.currentOutput()
|
||||
@@ -808,17 +825,21 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
}
|
||||
}
|
||||
|
||||
Logger.debug("[AcpTerminalManager] getOutput response:", {
|
||||
outputLength: response.output.length,
|
||||
truncated: response.truncated,
|
||||
hasExitStatus: !!response.exitStatus,
|
||||
})
|
||||
if (this.debug) {
|
||||
console.error("[AcpTerminalManager] getOutput response:", {
|
||||
outputLength: response.output.length,
|
||||
truncated: response.truncated,
|
||||
hasExitStatus: !!response.exitStatus,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
Logger.debug("[AcpTerminalManager] getOutput error:", errorMessage)
|
||||
if (this.debug) {
|
||||
console.error("[AcpTerminalManager] getOutput error:", errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
output: "",
|
||||
@@ -852,12 +873,16 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
}
|
||||
}
|
||||
|
||||
Logger.debug("[AcpTerminalManager] waitForExit:", { terminalId })
|
||||
if (this.debug) {
|
||||
console.error("[AcpTerminalManager] waitForExit:", { terminalId })
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await terminal.handle.waitForExit()
|
||||
|
||||
Logger.debug("[AcpTerminalManager] waitForExit response:", response)
|
||||
if (this.debug) {
|
||||
console.error("[AcpTerminalManager] waitForExit response:", response)
|
||||
}
|
||||
|
||||
return {
|
||||
exitCode: response.exitCode ?? undefined,
|
||||
@@ -867,7 +892,9 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
Logger.debug("[AcpTerminalManager] waitForExit error:", errorMessage)
|
||||
if (this.debug) {
|
||||
console.error("[AcpTerminalManager] waitForExit error:", errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
@@ -904,18 +931,24 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
}
|
||||
}
|
||||
|
||||
Logger.debug("[AcpTerminalManager] kill:", { terminalId })
|
||||
if (this.debug) {
|
||||
console.error("[AcpTerminalManager] kill:", { terminalId })
|
||||
}
|
||||
|
||||
try {
|
||||
await terminal.handle.kill()
|
||||
|
||||
Logger.debug("[AcpTerminalManager] kill success")
|
||||
if (this.debug) {
|
||||
console.error("[AcpTerminalManager] kill success")
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
Logger.debug("[AcpTerminalManager] kill error:", errorMessage)
|
||||
if (this.debug) {
|
||||
console.error("[AcpTerminalManager] kill error:", errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
@@ -949,19 +982,25 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
Logger.debug("[AcpTerminalManager] release:", { terminalId })
|
||||
if (this.debug) {
|
||||
console.error("[AcpTerminalManager] release:", { terminalId })
|
||||
}
|
||||
|
||||
try {
|
||||
await terminal.handle.release()
|
||||
terminal.released = true
|
||||
|
||||
Logger.debug("[AcpTerminalManager] release success")
|
||||
if (this.debug) {
|
||||
console.error("[AcpTerminalManager] release success")
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
Logger.debug("[AcpTerminalManager] release error:", errorMessage)
|
||||
if (this.debug) {
|
||||
console.error("[AcpTerminalManager] release error:", errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
@@ -1002,12 +1041,16 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
async releaseAll(): Promise<void> {
|
||||
const activeTerminals = this.getActiveTerminals()
|
||||
|
||||
Logger.debug("[AcpTerminalManager] releaseAll:", { count: activeTerminals.length })
|
||||
if (this.debug) {
|
||||
console.error("[AcpTerminalManager] releaseAll:", { count: activeTerminals.length })
|
||||
}
|
||||
|
||||
const releasePromises = activeTerminals.map((terminal) => this.release(terminal.id))
|
||||
await Promise.allSettled(releasePromises)
|
||||
|
||||
Logger.debug("[AcpTerminalManager] releaseAll complete")
|
||||
if (this.debug) {
|
||||
console.error("[AcpTerminalManager] releaseAll complete")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4,32 +4,14 @@
|
||||
* When the CLI is invoked with `--acp`, this module sets up the ACP connection
|
||||
* and runs Cline as an ACP-compliant agent communicating over stdio.
|
||||
*
|
||||
* This module exports:
|
||||
* - `ClineAgent` - Decoupled agent for programmatic use (no stdio dependency)
|
||||
* - `AcpAgent` - Thin wrapper that bridges stdio connection to ClineAgent
|
||||
* - `ClineSessionEmitter` - Typed EventEmitter for per-session events
|
||||
* - `runAcpMode` - Function to run Cline in stdio-based ACP mode
|
||||
*
|
||||
* @module acp
|
||||
*/
|
||||
|
||||
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"
|
||||
export type {
|
||||
AcpAgentOptions,
|
||||
AcpSessionState,
|
||||
ClineAgentOptions,
|
||||
ClineSessionEvents,
|
||||
PermissionHandler,
|
||||
} from "../agent/types.js"
|
||||
export { AcpAgent } from "./AcpAgent.js"
|
||||
|
||||
/** Original console methods for restoration if needed */
|
||||
const originalConsole = {
|
||||
log: console.log,
|
||||
@@ -69,8 +51,6 @@ 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
|
||||
}
|
||||
@@ -97,8 +77,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)
|
||||
@@ -114,7 +94,7 @@ export async function runAcpMode(options: AcpModeOptions = {}): Promise<void> {
|
||||
await agent?.shutdown()
|
||||
restoreConsole()
|
||||
} catch (error) {
|
||||
Logger.error("[ACP] Error during shutdown:", error)
|
||||
console.error("[ACP] Error during shutdown:", error)
|
||||
}
|
||||
|
||||
process.exit(0)
|
||||
@@ -133,9 +113,9 @@ export async function runAcpMode(options: AcpModeOptions = {}): Promise<void> {
|
||||
|
||||
// Handle stdin errors
|
||||
process.stdin.on("error", async (error) => {
|
||||
Logger.error("[ACP] stdin error:", error)
|
||||
console.error("[ACP] stdin error:", error)
|
||||
await shutdown()
|
||||
})
|
||||
|
||||
Logger.info("[ACP] Process is now listening for ACP requests on stdin")
|
||||
console.error("[ACP] Process is now listening for ACP requests on stdin")
|
||||
}
|
||||
@@ -12,7 +12,6 @@ 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
|
||||
@@ -176,7 +175,8 @@ describe("createSessionState", () => {
|
||||
const state = createSessionState("my-session-123")
|
||||
|
||||
expect(state.sessionId).toBe("my-session-123")
|
||||
expect(state.status).toBe(AcpSessionStatus.Idle)
|
||||
expect(state.isProcessing).toBe(false)
|
||||
expect(state.cancelled).toBe(false)
|
||||
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.status = AcpSessionStatus.Processing
|
||||
state1.isProcessing = true
|
||||
state1.pendingToolCalls.set("tool-1", {} as acp.ToolCall)
|
||||
|
||||
// state2 should be unaffected
|
||||
expect(state2.status).toBe(AcpSessionStatus.Idle)
|
||||
expect(state2.isProcessing).toBe(false)
|
||||
expect(state2.pendingToolCalls.size).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -758,7 +758,7 @@ describe("translateMessage - say messages", () => {
|
||||
expect(messageChunk).toBeDefined()
|
||||
|
||||
const chunk = messageChunk as acp.ContentChunk & { sessionUpdate: "agent_message_chunk" }
|
||||
expect((chunk.content as acp.TextContent).text).toBe("\nTask completed successfully!")
|
||||
expect((chunk.content as acp.TextContent).text).toBe("Task completed successfully!")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
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.
|
||||
@@ -180,7 +179,7 @@ export function translateMessage(
|
||||
function translateSayMessage(
|
||||
message: ClineMessage,
|
||||
sessionState: AcpSessionState,
|
||||
_options?: TranslateMessageOptions,
|
||||
options?: TranslateMessageOptions,
|
||||
): TranslatedMessage {
|
||||
const updates: acp.SessionUpdate[] = []
|
||||
let toolCallId: string | undefined
|
||||
@@ -230,11 +229,11 @@ function translateSayMessage(
|
||||
|
||||
case "completion_result":
|
||||
// Task completion - no direct update needed, handled by stopReason in prompt response
|
||||
// But we can send a final message chunk with a leading newline to separate from previous content
|
||||
// But we can send a final message chunk
|
||||
if (message.text) {
|
||||
updates.push({
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: "\n" + message.text },
|
||||
content: { type: "text", text: message.text },
|
||||
})
|
||||
}
|
||||
break
|
||||
@@ -313,10 +312,6 @@ function translateSayMessage(
|
||||
// API request finished - no specific update needed
|
||||
break
|
||||
|
||||
case "subagent_usage":
|
||||
// Hidden aggregate metrics event used for task-level accounting.
|
||||
break
|
||||
|
||||
case "task":
|
||||
// Task started - don't echo the user's prompt back to them
|
||||
// The ACP client already knows what they typed
|
||||
@@ -405,6 +400,7 @@ function translateAskMessage(
|
||||
switch (ask) {
|
||||
case "followup":
|
||||
case "plan_mode_respond":
|
||||
case "act_mode_respond":
|
||||
// These are questions to the user - send as agent message and await next prompt
|
||||
if (message.text) {
|
||||
let textToSend = message.text
|
||||
@@ -432,11 +428,6 @@ function translateAskMessage(
|
||||
}
|
||||
break
|
||||
|
||||
case "act_mode_respond":
|
||||
// act_mode_respond signals the turn is complete but its text content was already
|
||||
// sent via the say: "text" message. Don't send it again to avoid duplicate output.
|
||||
break
|
||||
|
||||
case "command":
|
||||
// Command permission request → tool_call + request_permission
|
||||
{
|
||||
@@ -619,14 +610,6 @@ function translateAskMessage(
|
||||
break
|
||||
|
||||
case "completion_result":
|
||||
// Completion result needs a leading newline to separate from previous content
|
||||
if (message.text) {
|
||||
updates.push({
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: "\n" + message.text },
|
||||
})
|
||||
}
|
||||
break
|
||||
case "resume_task":
|
||||
case "resume_completed_task":
|
||||
case "new_task":
|
||||
@@ -1020,7 +1003,8 @@ export function translateMessages(messages: ClineMessage[], sessionState: AcpSes
|
||||
export function createSessionState(sessionId: string): AcpSessionState {
|
||||
return {
|
||||
sessionId,
|
||||
status: AcpSessionStatus.Idle,
|
||||
isProcessing: false,
|
||||
cancelled: false,
|
||||
pendingToolCalls: new Map(),
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@
|
||||
import type * as acp from "@agentclientprotocol/sdk"
|
||||
import type { ClineAsk } from "@shared/ExtensionMessage"
|
||||
import type { ClineAskResponse } from "@shared/WebviewMessage"
|
||||
import { Logger } from "@/shared/services/Logger.js"
|
||||
import type { AcpSessionState, ClinePermissionOption } from "./types.js"
|
||||
|
||||
/**
|
||||
@@ -151,7 +150,7 @@ export function handlePermissionResponse(response: acp.RequestPermissionResponse
|
||||
|
||||
default:
|
||||
// Unknown option ID - treat as rejection for safety
|
||||
Logger.error(`[permissionHandler] Unknown permission option: ${optionId}`)
|
||||
console.error(`[permissionHandler] Unknown permission option: ${optionId}`)
|
||||
return {
|
||||
response: "noButtonClicked",
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Custom types and extensions for ACP integration with Cline CLI.
|
||||
*
|
||||
* This file extends the base ACP types with Cline-specific functionality.
|
||||
*/
|
||||
|
||||
import type * as acp from "@agentclientprotocol/sdk"
|
||||
import type { Controller } from "@/core/controller"
|
||||
|
||||
// Re-export common ACP types for convenience
|
||||
export type {
|
||||
Agent,
|
||||
AgentSideConnection,
|
||||
AudioContent,
|
||||
CancelNotification,
|
||||
ContentBlock,
|
||||
ImageContent,
|
||||
InitializeRequest,
|
||||
InitializeResponse,
|
||||
LoadSessionRequest,
|
||||
LoadSessionResponse,
|
||||
McpServer,
|
||||
ModelInfo,
|
||||
NewSessionRequest,
|
||||
NewSessionResponse,
|
||||
PermissionOption,
|
||||
PermissionOptionKind,
|
||||
PromptRequest,
|
||||
PromptResponse,
|
||||
ReadTextFileRequest,
|
||||
ReadTextFileResponse,
|
||||
RequestPermissionRequest,
|
||||
RequestPermissionResponse,
|
||||
SessionConfigOption,
|
||||
SessionModelState,
|
||||
SessionNotification,
|
||||
SessionUpdate,
|
||||
SetSessionConfigOptionRequest,
|
||||
SetSessionConfigOptionResponse,
|
||||
SetSessionModelRequest,
|
||||
SetSessionModelResponse,
|
||||
SetSessionModeRequest,
|
||||
SetSessionModeResponse,
|
||||
StopReason,
|
||||
TextContent,
|
||||
ToolCall,
|
||||
ToolCallStatus,
|
||||
ToolCallUpdate,
|
||||
ToolKind,
|
||||
WriteTextFileRequest,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>
|
||||
}
|
||||
@@ -131,9 +131,9 @@ const BUTTON_CONFIGS: Record<string, ButtonConfig> = {
|
||||
sendingDisabled: false,
|
||||
enableButtons: true,
|
||||
primaryText: "Start New Task",
|
||||
secondaryText: "Exit",
|
||||
secondaryText: undefined,
|
||||
primaryAction: "new_task",
|
||||
secondaryAction: "reject",
|
||||
secondaryAction: undefined,
|
||||
},
|
||||
resume_task: {
|
||||
sendingDisabled: false,
|
||||
@@ -155,9 +155,9 @@ const BUTTON_CONFIGS: Record<string, ButtonConfig> = {
|
||||
sendingDisabled: false,
|
||||
enableButtons: true,
|
||||
primaryText: "Start New Task with Context",
|
||||
secondaryText: "Exit",
|
||||
secondaryText: undefined,
|
||||
primaryAction: "new_task",
|
||||
secondaryAction: "reject",
|
||||
secondaryAction: undefined,
|
||||
},
|
||||
|
||||
// Streaming/partial states
|
||||
@@ -279,10 +279,10 @@ interface ActionButtonsProps {
|
||||
|
||||
/**
|
||||
* Determine which buttons are actually visible based on config
|
||||
* Cancel is hidden in the CLI (ThinkingIndicator handles that with esc)
|
||||
* Some actions (cancel, new_task) are hidden in the CLI
|
||||
*/
|
||||
export function getVisibleButtons(config: ButtonConfig) {
|
||||
const hiddenActions = ["cancel"]
|
||||
const hiddenActions = ["cancel", "new_task"]
|
||||
const hasPrimary = !!config.primaryText && !hiddenActions.includes(config.primaryAction || "")
|
||||
const hasSecondary = !!config.secondaryText && !hiddenActions.includes(config.secondaryAction || "")
|
||||
return { hasPrimary, hasSecondary }
|
||||
@@ -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 { isEnterKey, isMouseEscapeSequence } from "../utils/input"
|
||||
import { isMouseEscapeSequence } from "../utils/input"
|
||||
|
||||
interface ApiKeyInputProps {
|
||||
providerName: string
|
||||
@@ -39,7 +39,7 @@ export const ApiKeyInput: React.FC<ApiKeyInputProps> = ({
|
||||
onCancel()
|
||||
return
|
||||
}
|
||||
if (isEnterKey(input, key)) {
|
||||
if (key.return) {
|
||||
onSubmit(value)
|
||||
return
|
||||
}
|
||||
@@ -82,8 +82,9 @@ describe("App", () => {
|
||||
})
|
||||
|
||||
it("should render AuthView when view is auth", () => {
|
||||
const { lastFrame } = render(<App controller={mockController} view="auth" />)
|
||||
const { lastFrame } = render(<App authQuickSetup={{ provider: "openai" }} controller={mockController} view="auth" />)
|
||||
expect(lastFrame()).toContain("AuthView")
|
||||
expect(lastFrame()).toContain("openai")
|
||||
})
|
||||
|
||||
it("should render ChatView when view is welcome", () => {
|
||||
@@ -3,15 +3,14 @@
|
||||
* Routes between different views (task, history, config)
|
||||
*/
|
||||
|
||||
import { Box, useApp } from "ink"
|
||||
import React, { ReactNode, useCallback, useEffect, useState } from "react"
|
||||
import { Box } from "ink"
|
||||
import React, { ReactNode, useCallback, 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"
|
||||
|
||||
@@ -82,6 +81,13 @@ interface AppProps {
|
||||
globalSkills?: SkillInfo[]
|
||||
localSkills?: SkillInfo[]
|
||||
onToggleSkill?: (isGlobal: boolean, skillPath: string, enabled: boolean) => void
|
||||
// For auth view
|
||||
authQuickSetup?: {
|
||||
provider?: string
|
||||
apikey?: string
|
||||
modelid?: string
|
||||
baseurl?: string
|
||||
}
|
||||
// For welcome view
|
||||
onWelcomeSubmit?: (prompt: string, imagePaths: string[]) => void
|
||||
onWelcomeExit?: () => void
|
||||
@@ -89,19 +95,11 @@ interface AppProps {
|
||||
initialImages?: string[]
|
||||
// Stdin support
|
||||
isRawModeSupported?: boolean
|
||||
// Robot position (calculated before Ink mounts)
|
||||
robotTopRow?: number
|
||||
}
|
||||
|
||||
export const App: React.FC<AppProps> = (props) => {
|
||||
const { exit } = useApp()
|
||||
|
||||
return (
|
||||
<ErrorBoundary exit={exit}>
|
||||
<InternalApp {...props} />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const InternalApp: React.FC<AppProps> = ({
|
||||
export const App: React.FC<AppProps> = ({
|
||||
view: initialView,
|
||||
taskId,
|
||||
verbose = false,
|
||||
@@ -137,26 +135,17 @@ const InternalApp: React.FC<AppProps> = ({
|
||||
globalSkills,
|
||||
localSkills,
|
||||
onToggleSkill,
|
||||
authQuickSetup,
|
||||
onWelcomeSubmit,
|
||||
onWelcomeExit,
|
||||
initialPrompt,
|
||||
initialImages,
|
||||
isRawModeSupported = true,
|
||||
robotTopRow,
|
||||
}) => {
|
||||
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)
|
||||
@@ -251,6 +240,7 @@ const InternalApp: React.FC<AppProps> = ({
|
||||
onComplete={onComplete}
|
||||
onError={onError}
|
||||
onNavigateToWelcome={handleNavigateToWelcome}
|
||||
quickSetup={authQuickSetup}
|
||||
/>
|
||||
)
|
||||
break
|
||||
@@ -264,11 +254,12 @@ const InternalApp: React.FC<AppProps> = ({
|
||||
) : (
|
||||
<ChatView
|
||||
controller={controller}
|
||||
initialImages={pendingInitialImages}
|
||||
initialPrompt={pendingInitialPrompt}
|
||||
initialImages={initialImages}
|
||||
initialPrompt={initialPrompt}
|
||||
onComplete={onComplete}
|
||||
onError={onError}
|
||||
onExit={onWelcomeExit}
|
||||
robotTopRow={robotTopRow}
|
||||
taskId={selectedTaskId}
|
||||
/>
|
||||
)}
|
||||
@@ -34,7 +34,8 @@ type AsciiMotionCliProps = {
|
||||
autoPlay?: boolean;
|
||||
loop?: boolean;
|
||||
onReady?: (api: PlaybackAPI) => void;
|
||||
onInteraction?: () => void; // Called when user scrolls, clicks, or drags
|
||||
robotTopRow?: number; // Screen row where robot is rendered (calculated before Ink mounts)
|
||||
onScroll?: () => void; // Called when user scrolls (scroll wheel)
|
||||
};
|
||||
|
||||
const FRAMES: FrameData[] = [
|
||||
@@ -333364,12 +333365,12 @@ const FRAME_BOTTOM_RIGHT = 128;
|
||||
|
||||
export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
|
||||
hasDarkBackground = true,
|
||||
onInteraction,
|
||||
robotTopRow,
|
||||
onScroll,
|
||||
}) => {
|
||||
const [frameIndex, setFrameIndex] = useState(0);
|
||||
const [targetFrame, setTargetFrame] = useState(0);
|
||||
const [cursor, setCursor] = useState({ x: 0, y: 0 });
|
||||
const lastCursorUpdateRef = useRef(0);
|
||||
const { stdout } = useStdout();
|
||||
const { stdin, setRawMode } = useStdin();
|
||||
|
||||
@@ -333378,9 +333379,8 @@ export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
|
||||
const faceX = Math.floor(terminalWidth / 2);
|
||||
// Robot height after cropping is ~12 rows, eyes are roughly halfway down
|
||||
const robotHeight = 12;
|
||||
// Robot is always rendered at the top of the terminal in welcome state.
|
||||
// faceY is the robot's eye level (row 2 + half robot height)
|
||||
const faceY = 2 + Math.floor(robotHeight / 2);
|
||||
// faceY is the robot's eye level - calculated from robotTopRow prop (set before Ink mounts)
|
||||
const faceY = robotTopRow !== undefined ? robotTopRow + Math.floor(robotHeight / 2) : null;
|
||||
|
||||
// Select color theme based on background
|
||||
const theme = useMemo(() => hasDarkBackground ? THEME_DARK : THEME_LIGHT, [hasDarkBackground]);
|
||||
@@ -333390,13 +333390,13 @@ export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
|
||||
// Stop animation on terminal resize to prevent visual glitches
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
onInteraction?.();
|
||||
onScroll?.();
|
||||
};
|
||||
process.stdout.on("resize", handleResize);
|
||||
return () => {
|
||||
process.stdout.off("resize", handleResize);
|
||||
};
|
||||
}, [onInteraction]);
|
||||
}, [onScroll]);
|
||||
|
||||
// Mouse tracking - gracefully handle environments without tty support
|
||||
useEffect(() => {
|
||||
@@ -333417,26 +333417,15 @@ export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
|
||||
const handleData = (data: Buffer) => {
|
||||
const str = data.toString();
|
||||
|
||||
// Parse mouse events: \x1b[<button;x;yM (M=press, m=release)
|
||||
// Parse mouse events: \x1b[<button;x;yM
|
||||
const mouseMatch = str.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
|
||||
if (mouseMatch) {
|
||||
const button = parseInt(mouseMatch[1], 10);
|
||||
const isPress = mouseMatch[4] === "M";
|
||||
// Button 64/65 = scroll up/down
|
||||
// Button 0-2 = left/middle/right click (on press)
|
||||
// Button 32-34 = drag with left/middle/right button held
|
||||
const isScroll = button === 64 || button === 65;
|
||||
const isClick = isPress && button >= 0 && button <= 2;
|
||||
const isDrag = button >= 32 && button <= 34;
|
||||
if (isScroll || isClick || isDrag) {
|
||||
onInteraction?.();
|
||||
}
|
||||
// Throttle cursor updates to ~20fps to reduce re-renders
|
||||
const now = Date.now();
|
||||
if (now - lastCursorUpdateRef.current >= 50) {
|
||||
lastCursorUpdateRef.current = now;
|
||||
setCursor({ x: parseInt(mouseMatch[2], 10), y: parseInt(mouseMatch[3], 10) });
|
||||
// Button 64 = scroll up, 65 = scroll down
|
||||
if (button === 64 || button === 65) {
|
||||
onScroll?.();
|
||||
}
|
||||
setCursor({ x: parseInt(mouseMatch[2], 10), y: parseInt(mouseMatch[3], 10) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -333455,6 +333444,9 @@ export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
|
||||
|
||||
// Map cursor position to target frame (continuous interpolation)
|
||||
useEffect(() => {
|
||||
// Wait until we know where the robot is on screen
|
||||
if (faceY === null) return;
|
||||
|
||||
const dx = cursor.x - faceX;
|
||||
const dy = cursor.y - faceY;
|
||||
|
||||
@@ -333577,8 +333569,7 @@ export const StaticRobotFrame: React.FC<{ hasDarkBackground?: boolean }> = ({
|
||||
const y = minY + originalIndex;
|
||||
const croppedRow = row.slice(minX, maxX + 1);
|
||||
// Manual centering for Static component compatibility
|
||||
// Use Math.round to match Ink's flexbox centering behavior
|
||||
const padding = Math.max(0, Math.round(((process.stdout.columns || 80) - robotWidth) / 2));
|
||||
const padding = Math.max(0, Math.floor(((process.stdout.columns || 80) - robotWidth) / 2));
|
||||
return (
|
||||
<Box key={y}>
|
||||
<Text>{" ".repeat(padding)}</Text>
|
||||
@@ -3,14 +3,15 @@
|
||||
* Handles different types of user interactions (text input, confirmations, choices)
|
||||
*/
|
||||
|
||||
import type { ClineAsk, ClineMessage } from "@shared/ExtensionMessage"
|
||||
import type { ClineAsk } from "@shared/ExtensionMessage"
|
||||
import { Box, Text, useApp, useInput } from "ink"
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import { useTaskController } from "../context/TaskContext"
|
||||
import { useLastCompletedAskMessage } from "../hooks/useStateSubscriber"
|
||||
import { isEnterKey, isMouseEscapeSequence } from "../utils/input"
|
||||
import { isMouseEscapeSequence } from "../utils/input"
|
||||
import { jsonParseSafe } from "../utils/parser"
|
||||
import { getCliMessagePrefixIcon } from "./MessageRow"
|
||||
|
||||
interface AskPromptProps {
|
||||
onRespond?: (response: string) => void
|
||||
@@ -136,7 +137,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 (isEnterKey(input, key)) {
|
||||
if (key.return) {
|
||||
// Submit free text on Enter
|
||||
if (textInput.trim()) {
|
||||
sendResponse("messageResponse", textInput.trim())
|
||||
@@ -145,7 +146,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 = Number.parseInt(input, 10)
|
||||
const num = 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 +157,7 @@ export const AskPrompt: React.FC<AskPromptProps> = ({ onRespond }) => {
|
||||
}
|
||||
} else if (promptType === "text") {
|
||||
// Text input mode
|
||||
if (isEnterKey(input, key)) {
|
||||
if (key.return) {
|
||||
// Submit on Enter
|
||||
if (textInput.trim()) {
|
||||
sendResponse("messageResponse", textInput.trim())
|
||||
@@ -169,7 +170,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 (isEnterKey(input, key)) {
|
||||
if (key.return) {
|
||||
// Submit on Enter
|
||||
if (textInput.trim()) {
|
||||
sendResponse("messageResponse", textInput.trim())
|
||||
@@ -185,7 +186,7 @@ export const AskPrompt: React.FC<AskPromptProps> = ({ onRespond }) => {
|
||||
}
|
||||
} else if (promptType === "completion") {
|
||||
// Task completed - allow follow-up question or exit
|
||||
if (isEnterKey(input, key)) {
|
||||
if (key.return) {
|
||||
if (textInput.trim()) {
|
||||
// Send follow-up question
|
||||
sendResponse("messageResponse", textInput.trim())
|
||||
@@ -371,72 +372,3 @@ export const AskPrompt: React.FC<AskPromptProps> = ({ onRespond }) => {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get emoji icon for message type
|
||||
*/
|
||||
function getCliMessagePrefixIcon(message: ClineMessage): string {
|
||||
if (message.type === "ask") {
|
||||
switch (message.ask) {
|
||||
case "followup":
|
||||
return "❓"
|
||||
case "command":
|
||||
case "command_output":
|
||||
return "⚙️"
|
||||
case "tool":
|
||||
return "🔧"
|
||||
case "completion_result":
|
||||
return "✅"
|
||||
case "api_req_failed":
|
||||
return "❌"
|
||||
case "resume_task":
|
||||
case "resume_completed_task":
|
||||
return "▶️"
|
||||
case "browser_action_launch":
|
||||
return "🌐"
|
||||
case "use_mcp_server":
|
||||
return "🔌"
|
||||
case "plan_mode_respond":
|
||||
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 " "
|
||||
}
|
||||
}
|
||||
@@ -6,36 +6,24 @@
|
||||
import { Box, Text, useApp, useInput } from "ink"
|
||||
import Spinner from "ink-spinner"
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { refreshOcaModels } from "@/core/controller/models/refreshOcaModels"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { openAiCodexDefaultModelId, openRouterDefaultModelId } from "@/shared/api"
|
||||
import { StringRequest } from "@/shared/proto/cline/common"
|
||||
import { API_PROVIDERS_LIST, openAiCodexDefaultModelId, openRouterDefaultModelId } from "@/shared/api"
|
||||
import { ProviderToApiKeyMap } from "@/shared/storage"
|
||||
import { openExternal } from "@/utils/env"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { getAllFeaturedModels } from "../constants/featured-models"
|
||||
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 { isEnterKey, isMouseEscapeSequence } from "../utils/input"
|
||||
import { applyBedrockConfig, applyProviderConfig } from "../utils/provider-config"
|
||||
import { useValidProviders } from "../utils/providers"
|
||||
import { isMouseEscapeSequence } from "../utils/input"
|
||||
import { ApiKeyInput } from "./ApiKeyInput"
|
||||
import { StaticRobotFrame } from "./AsciiMotionCli"
|
||||
import { BedrockCustomModelFlow } from "./BedrockCustomModelFlow"
|
||||
import { type BedrockConfig, BedrockSetup } from "./BedrockSetup"
|
||||
import {
|
||||
FeaturedModelPicker,
|
||||
getFeaturedModelAtIndex,
|
||||
getFeaturedModelMaxIndex,
|
||||
isBrowseAllSelected,
|
||||
} from "./FeaturedModelPicker"
|
||||
import { ImportView } from "./ImportView"
|
||||
import { CUSTOM_MODEL_ID, getDefaultModelId, hasModelPicker, ModelPicker } from "./ModelPicker"
|
||||
import { OcaEmployeeCheck } from "./OcaEmployeeCheck"
|
||||
import { getProviderLabel } from "./ProviderPicker"
|
||||
import { getDefaultModelId, hasModelPicker, ModelPicker } from "./ModelPicker"
|
||||
import { getProviderLabel, getProviderOrder } from "./ProviderPicker"
|
||||
|
||||
type AuthStep =
|
||||
| "menu"
|
||||
@@ -47,19 +35,26 @@ type AuthStep =
|
||||
| "success"
|
||||
| "error"
|
||||
| "cline_auth"
|
||||
| "oca_employee_check"
|
||||
| "oca_auth"
|
||||
| "cline_model"
|
||||
| "openai_codex_auth"
|
||||
| "bedrock"
|
||||
| "import"
|
||||
| "bedrock_custom"
|
||||
|
||||
// Featured models loaded from shared constants
|
||||
const featuredModels = getAllFeaturedModels()
|
||||
|
||||
interface AuthViewProps {
|
||||
controller: any
|
||||
onComplete?: () => void
|
||||
onError?: () => void
|
||||
onNavigateToWelcome?: () => void
|
||||
// Quick setup options
|
||||
quickSetup?: {
|
||||
provider?: string
|
||||
apikey?: string
|
||||
modelid?: string
|
||||
baseurl?: string
|
||||
}
|
||||
}
|
||||
|
||||
interface SelectItem {
|
||||
@@ -84,7 +79,7 @@ const Select: React.FC<{
|
||||
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)) {
|
||||
} else if (key.return) {
|
||||
onSelect(items[selectedIndex].value)
|
||||
}
|
||||
},
|
||||
@@ -130,7 +125,7 @@ const TextInput: React.FC<{
|
||||
return
|
||||
}
|
||||
|
||||
if (isEnterKey(input, key)) {
|
||||
if (key.return) {
|
||||
onSubmit(value)
|
||||
} else if (key.backspace || key.delete) {
|
||||
onChange(value.slice(0, -1))
|
||||
@@ -145,22 +140,15 @@ const TextInput: React.FC<{
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{!displayValue && placeholder ? (
|
||||
<Text color="gray">e.g. {placeholder}</Text>
|
||||
) : (
|
||||
<Text color="white">{displayValue || ""}</Text>
|
||||
)}
|
||||
<Text color="white">{displayValue || placeholder || ""}</Text>
|
||||
<Text inverse> </Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onError, onNavigateToWelcome }) => {
|
||||
export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onError, onNavigateToWelcome, quickSetup }) => {
|
||||
const { exit } = useApp()
|
||||
|
||||
const providers = useValidProviders()
|
||||
|
||||
const [step, setStep] = useState<AuthStep>("menu")
|
||||
const [step, setStep] = useState<AuthStep>(quickSetup ? "saving" : "menu")
|
||||
const [selectedProvider, setSelectedProvider] = useState<string>(
|
||||
StateManager.get().getApiConfiguration().actModeApiProvider ||
|
||||
StateManager.get().getApiConfiguration().planModeApiProvider ||
|
||||
@@ -170,43 +158,23 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
const [modelId, setModelId] = useState("")
|
||||
const [baseUrl, setBaseUrl] = useState("")
|
||||
const [errorMessage, setErrorMessage] = useState("")
|
||||
const [authStatus, setAuthStatus] = useState<string>("")
|
||||
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)
|
||||
|
||||
// OCA auth hook - enabled when step is oca_auth
|
||||
const handleOcaAuthSuccess = useCallback(async () => {
|
||||
await applyProviderConfig({ providerId: "oca", controller })
|
||||
// Fetch OCA models from the API - this sets actModeOcaModelId/planModeOcaModelId in state
|
||||
await refreshOcaModels(controller, StringRequest.create({ value: "" }))
|
||||
const stateManager = StateManager.get()
|
||||
stateManager.setGlobalState("welcomeViewCompleted", true)
|
||||
await stateManager.flushPendingState()
|
||||
setSelectedProvider("oca")
|
||||
const actModelId = stateManager.getGlobalSettingsKey("actModeOcaModelId") || ""
|
||||
setModelId(actModelId)
|
||||
setStep("success")
|
||||
}, [controller])
|
||||
|
||||
const handleOcaAuthError = useCallback((error: Error) => {
|
||||
setErrorMessage(error.message)
|
||||
setStep("error")
|
||||
// Use providers.json order, filtered to only available providers
|
||||
const sortedProviders = useMemo(() => {
|
||||
const availableProviders = new Set(API_PROVIDERS_LIST)
|
||||
return getProviderOrder().filter((p) => availableProviders.has(p))
|
||||
}, [])
|
||||
|
||||
const { startAuth: initiateOcaAuth } = useOcaAuth({
|
||||
controller,
|
||||
enabled: step === "oca_auth",
|
||||
onSuccess: handleOcaAuthSuccess,
|
||||
onError: handleOcaAuthError,
|
||||
})
|
||||
|
||||
// Main menu items - conditionally include import options
|
||||
const mainMenuItems: SelectItem[] = useMemo(() => {
|
||||
const items: SelectItem[] = [{ label: "Sign in with Cline", value: "cline_auth" }]
|
||||
const items: SelectItem[] = [{ label: "Sign in with Cline account", value: "cline_auth" }]
|
||||
|
||||
// Add OpenAI Codex option for ChatGPT subscribers
|
||||
items.push({ label: "Sign in with ChatGPT Subscription", value: "openai_codex_auth" })
|
||||
@@ -229,13 +197,15 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
const providerItems: SelectItem[] = useMemo(() => {
|
||||
const search = providerSearch.toLowerCase()
|
||||
const filtered = providerSearch
|
||||
? providers.filter((p) => p.toLowerCase().includes(search) || getProviderLabel(p).toLowerCase().includes(search))
|
||||
: providers
|
||||
? sortedProviders.filter(
|
||||
(p) => p.toLowerCase().includes(search) || getProviderLabel(p).toLowerCase().includes(search),
|
||||
)
|
||||
: sortedProviders
|
||||
return filtered.map((p: string) => ({
|
||||
label: getProviderLabel(p),
|
||||
value: p,
|
||||
}))
|
||||
}, [providers, providerSearch])
|
||||
}, [sortedProviders, providerSearch])
|
||||
|
||||
// Use shared scrollable list hook for provider windowing
|
||||
const TOTAL_PROVIDER_ROWS = 8
|
||||
@@ -256,7 +226,6 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
}, [])
|
||||
|
||||
// Reset provider index when search changes
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: we want to reset here
|
||||
useEffect(() => {
|
||||
setProviderIndex(0)
|
||||
}, [providerSearch])
|
||||
@@ -268,6 +237,13 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
}
|
||||
}, [step, selectedProvider])
|
||||
|
||||
// Handle quick setup
|
||||
useEffect(() => {
|
||||
if (quickSetup && step === "saving") {
|
||||
handleQuickSetup()
|
||||
}
|
||||
}, [quickSetup, step])
|
||||
|
||||
// Subscribe to auth status updates when in cline_auth step
|
||||
useEffect(() => {
|
||||
if (step !== "cline_auth") {
|
||||
@@ -282,11 +258,22 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
return
|
||||
}
|
||||
|
||||
if (authState.user?.email) {
|
||||
// Auth succeeded - save configuration and transition to model selection
|
||||
await applyProviderConfig({ providerId: "cline", controller })
|
||||
if (authState.user && authState.user.email) {
|
||||
// Auth succeeded - save configuration and transition to success
|
||||
const stateManager = StateManager.get()
|
||||
const mode = stateManager.getGlobalSettingsKey("mode") || "act"
|
||||
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
const modelIdKey = mode === "act" ? "actModeApiModelId" : "planModeApiModelId"
|
||||
const config: Record<string, string> = {
|
||||
actModeApiProvider: "cline",
|
||||
[providerKey]: "cline",
|
||||
[modelIdKey]: openRouterDefaultModelId,
|
||||
}
|
||||
stateManager.setApiConfiguration(config)
|
||||
stateManager.flushPendingState()
|
||||
|
||||
setSelectedProvider("cline")
|
||||
setModelId(openRouterDefaultModelId)
|
||||
setModelId(config[modelIdKey])
|
||||
setStep("cline_model")
|
||||
}
|
||||
}
|
||||
@@ -300,6 +287,80 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
}
|
||||
}, [step, controller])
|
||||
|
||||
const handleQuickSetup = async () => {
|
||||
if (!quickSetup) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { provider, apikey, modelid, baseurl } = quickSetup
|
||||
|
||||
// Validate required parameters
|
||||
if (!provider || !apikey || !modelid) {
|
||||
setErrorMessage("Quick setup requires --provider, --apikey, and --modelid flags")
|
||||
setStep("error")
|
||||
return
|
||||
}
|
||||
|
||||
const normalizedProvider = provider.toLowerCase().trim()
|
||||
|
||||
if (!sortedProviders.includes(normalizedProvider)) {
|
||||
setErrorMessage(`Invalid provider '${provider}'. Supported providers: ${sortedProviders.join(", ")}`)
|
||||
setStep("error")
|
||||
return
|
||||
}
|
||||
|
||||
if (normalizedProvider === "bedrock") {
|
||||
setErrorMessage(
|
||||
"Bedrock provider is not supported for quick setup due to complex authentication requirements. Please use interactive setup.",
|
||||
)
|
||||
setStep("error")
|
||||
return
|
||||
}
|
||||
|
||||
if (baseurl && !["openai", "openai-native"].includes(normalizedProvider)) {
|
||||
setErrorMessage("Base URL is only supported for OpenAI and OpenAI-compatible providers")
|
||||
setStep("error")
|
||||
return
|
||||
}
|
||||
|
||||
// Save configuration
|
||||
const stateManager = StateManager.get()
|
||||
const config: Record<string, string> = {
|
||||
actModeApiProvider: normalizedProvider,
|
||||
planModeApiProvider: normalizedProvider,
|
||||
actModeApiModelId: modelid,
|
||||
planModeApiModelId: modelid,
|
||||
}
|
||||
|
||||
// Use provider-specific API key field
|
||||
const keyField = ProviderToApiKeyMap[normalizedProvider]
|
||||
if (keyField) {
|
||||
const fields = Array.isArray(keyField) ? keyField : [keyField]
|
||||
// Set the first key field for the provider
|
||||
config[fields[0]] = apikey
|
||||
} else {
|
||||
// Fallback to generic apiKey
|
||||
config.apiKey = apikey
|
||||
}
|
||||
|
||||
if (baseurl) {
|
||||
config.openAiBaseUrl = baseurl
|
||||
}
|
||||
|
||||
stateManager.setApiConfiguration(config)
|
||||
|
||||
await stateManager.flushPendingState()
|
||||
setSelectedProvider(normalizedProvider)
|
||||
setModelId(modelid)
|
||||
setBaseUrl(baseurl || "")
|
||||
setStep("success")
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : String(error))
|
||||
setStep("error")
|
||||
}
|
||||
}
|
||||
|
||||
// Start OpenAI Codex OAuth flow
|
||||
const startOpenAiCodexAuth = useCallback(async () => {
|
||||
try {
|
||||
@@ -313,10 +374,19 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
await openAiCodexOAuthManager.waitForCallback()
|
||||
|
||||
// Success - save configuration
|
||||
await applyProviderConfig({ providerId: "openai-codex", controller })
|
||||
const stateManager = StateManager.get()
|
||||
stateManager.setGlobalState("welcomeViewCompleted", true)
|
||||
const mode = stateManager.getGlobalSettingsKey("mode") || "act"
|
||||
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
const modelIdKey = mode === "act" ? "actModeApiModelId" : "planModeApiModelId"
|
||||
const config: Record<string, string> = {
|
||||
actModeApiProvider: "openai-codex",
|
||||
planModeApiProvider: "openai-codex",
|
||||
[providerKey]: "openai-codex",
|
||||
[modelIdKey]: openAiCodexDefaultModelId,
|
||||
}
|
||||
stateManager.setApiConfiguration(config)
|
||||
await stateManager.flushPendingState()
|
||||
|
||||
setSelectedProvider("openai-codex")
|
||||
setModelId(openAiCodexDefaultModelId)
|
||||
setStep("success")
|
||||
@@ -331,6 +401,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
const startClineAuth = useCallback(async () => {
|
||||
try {
|
||||
setStep("cline_auth")
|
||||
setAuthStatus("Starting authentication...")
|
||||
await AuthService.getInstance(controller).createAuthRequest()
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : String(error))
|
||||
@@ -338,11 +409,6 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
}
|
||||
}, [controller])
|
||||
|
||||
const startOcaAuth = useCallback(() => {
|
||||
setStep("oca_auth")
|
||||
initiateOcaAuth()
|
||||
}, [initiateOcaAuth])
|
||||
|
||||
const handleMainMenuSelect = useCallback(
|
||||
(value: string) => {
|
||||
if (value === "exit") {
|
||||
@@ -369,9 +435,8 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
const handleProviderSelect = useCallback(
|
||||
(value: string) => {
|
||||
setSelectedProvider(value)
|
||||
if (value === "oca") {
|
||||
// Show employee check screen before starting auth
|
||||
setStep("oca_employee_check")
|
||||
if (value === "cline") {
|
||||
startClineAuth()
|
||||
} else if (value === "openai-codex") {
|
||||
setStep("openai_codex_auth")
|
||||
startOpenAiCodexAuth()
|
||||
@@ -381,7 +446,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
setStep("apikey")
|
||||
}
|
||||
},
|
||||
[startOcaAuth, startOpenAiCodexAuth],
|
||||
[startClineAuth, startOpenAiCodexAuth],
|
||||
)
|
||||
|
||||
const handleApiKeySubmit = useCallback(
|
||||
@@ -398,54 +463,42 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
[selectedProvider],
|
||||
)
|
||||
|
||||
// Save custom Bedrock ARN configuration with base model for capability detection
|
||||
const saveCustomBedrockConfiguration = useCallback(
|
||||
async (arn: string, baseModelId: string) => {
|
||||
try {
|
||||
if (!bedrockConfig) {
|
||||
throw new Error("Bedrock configuration is missing")
|
||||
}
|
||||
await applyBedrockConfig({
|
||||
bedrockConfig,
|
||||
modelId: arn,
|
||||
customModelBaseId: baseModelId,
|
||||
controller,
|
||||
})
|
||||
|
||||
const stateManager = StateManager.get()
|
||||
stateManager.setGlobalState("welcomeViewCompleted", true)
|
||||
await stateManager.flushPendingState()
|
||||
|
||||
setStep("success")
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : String(error))
|
||||
setStep("error")
|
||||
}
|
||||
},
|
||||
[bedrockConfig, controller],
|
||||
)
|
||||
|
||||
const saveConfiguration = useCallback(
|
||||
async (model: string, base: string) => {
|
||||
try {
|
||||
if (selectedProvider === "bedrock" && bedrockConfig) {
|
||||
await applyBedrockConfig({
|
||||
bedrockConfig,
|
||||
modelId: model,
|
||||
controller,
|
||||
})
|
||||
} else {
|
||||
await applyProviderConfig({
|
||||
providerId: selectedProvider,
|
||||
apiKey,
|
||||
modelId: model,
|
||||
baseUrl: base,
|
||||
controller,
|
||||
})
|
||||
const stateManager = StateManager.get()
|
||||
const config: Record<string, string> = {
|
||||
actModeApiProvider: selectedProvider,
|
||||
planModeApiProvider: selectedProvider,
|
||||
actModeApiModelId: model,
|
||||
planModeApiModelId: model,
|
||||
apiProvider: selectedProvider,
|
||||
}
|
||||
|
||||
const stateManager = StateManager.get()
|
||||
stateManager.setGlobalState("welcomeViewCompleted", true)
|
||||
// Add API key or Bedrock-specific config
|
||||
if (selectedProvider === "bedrock" && bedrockConfig) {
|
||||
const bedrockFields: Record<string, unknown> = {
|
||||
awsAuthentication: bedrockConfig.awsAuthentication,
|
||||
awsRegion: bedrockConfig.awsRegion,
|
||||
awsUseCrossRegionInference: bedrockConfig.awsUseCrossRegionInference,
|
||||
}
|
||||
if (bedrockConfig.awsProfile !== undefined) bedrockFields.awsProfile = bedrockConfig.awsProfile
|
||||
if (bedrockConfig.awsAccessKey) bedrockFields.awsAccessKey = bedrockConfig.awsAccessKey
|
||||
if (bedrockConfig.awsSecretKey) bedrockFields.awsSecretKey = bedrockConfig.awsSecretKey
|
||||
if (bedrockConfig.awsSessionToken) bedrockFields.awsSessionToken = bedrockConfig.awsSessionToken
|
||||
Object.assign(config, bedrockFields)
|
||||
} else if (apiKey) {
|
||||
const keyField = ProviderToApiKeyMap[selectedProvider as keyof typeof ProviderToApiKeyMap]
|
||||
if (keyField) {
|
||||
const fields = Array.isArray(keyField) ? keyField : [keyField]
|
||||
config[fields[0]] = apiKey
|
||||
}
|
||||
}
|
||||
|
||||
if (base) {
|
||||
config.openAiBaseUrl = base
|
||||
}
|
||||
stateManager.setApiConfiguration(config)
|
||||
await stateManager.flushPendingState()
|
||||
|
||||
setStep("success")
|
||||
@@ -454,17 +507,11 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
setStep("error")
|
||||
}
|
||||
},
|
||||
[selectedProvider, apiKey, bedrockConfig, controller],
|
||||
[selectedProvider, apiKey, bedrockConfig],
|
||||
)
|
||||
|
||||
const handleModelIdSubmit = useCallback(
|
||||
(value: string) => {
|
||||
// Intercept "Custom" selection for Bedrock — redirect to custom ARN input flow
|
||||
if (value === CUSTOM_MODEL_ID && selectedProvider === "bedrock") {
|
||||
setStep("bedrock_custom")
|
||||
return
|
||||
}
|
||||
|
||||
if (value.trim()) {
|
||||
setModelId(value)
|
||||
}
|
||||
@@ -512,18 +559,11 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
}, [])
|
||||
|
||||
// Auto-navigate to welcome after success (immediate)
|
||||
// For quick setup mode (no onNavigateToWelcome), exit the Ink app
|
||||
useEffect(() => {
|
||||
if (step === "success") {
|
||||
if (onNavigateToWelcome) {
|
||||
onNavigateToWelcome()
|
||||
} else {
|
||||
// Quick setup mode - exit Ink app after successful configuration
|
||||
// The cleanup handler in runInkApp will handle process exit
|
||||
exit()
|
||||
}
|
||||
if (step === "success" && onNavigateToWelcome) {
|
||||
onNavigateToWelcome()
|
||||
}
|
||||
}, [step, onNavigateToWelcome, exit])
|
||||
}, [step, onNavigateToWelcome])
|
||||
|
||||
// Error screen menu items
|
||||
const errorMenuItems: SelectItem[] = useMemo(() => {
|
||||
@@ -572,9 +612,6 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
// Go back to cline_model if we came from there (Cline provider)
|
||||
if (selectedProvider === "cline") {
|
||||
setStep("cline_model")
|
||||
} else if (selectedProvider === "bedrock") {
|
||||
// Bedrock skips the API key step — go back to Bedrock setup
|
||||
setStep("bedrock")
|
||||
} else {
|
||||
setStep("apikey")
|
||||
}
|
||||
@@ -583,12 +620,6 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
setBaseUrl("")
|
||||
setStep("modelid")
|
||||
break
|
||||
case "oca_employee_check":
|
||||
setStep("provider")
|
||||
break
|
||||
case "oca_auth":
|
||||
setStep("oca_employee_check")
|
||||
break
|
||||
case "cline_auth":
|
||||
setStep("menu")
|
||||
break
|
||||
@@ -691,7 +722,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
<Box flexDirection="column">
|
||||
<Text color="white">Model ID</Text>
|
||||
<Text> </Text>
|
||||
<Text color="gray">e.g., claude-sonnet-4-6, gpt-4o</Text>
|
||||
<Text color="gray">e.g., claude-sonnet-4-20250514, gpt-4o</Text>
|
||||
<Text> </Text>
|
||||
<TextInput onChange={setModelId} onSubmit={handleModelIdSubmit} placeholder="model-id" value={modelId} />
|
||||
<Text> </Text>
|
||||
@@ -727,10 +758,6 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
</Box>
|
||||
)
|
||||
|
||||
case "oca_employee_check":
|
||||
return <OcaEmployeeCheck isActive={step === "oca_employee_check"} onCancel={goBack} onSignIn={startOcaAuth} />
|
||||
|
||||
case "oca_auth":
|
||||
case "cline_auth":
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
@@ -765,11 +792,50 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
)
|
||||
|
||||
case "cline_model": {
|
||||
const allModels = featuredModels
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color="white">Choose a model</Text>
|
||||
<Text> </Text>
|
||||
<FeaturedModelPicker featuredModels={featuredModels} selectedIndex={clineModelIndex} />
|
||||
|
||||
{/* Model list */}
|
||||
{allModels.map((model, i) => (
|
||||
<Box flexDirection="column" key={model.id} marginBottom={1}>
|
||||
<Box>
|
||||
<Text color={i === clineModelIndex ? COLORS.primaryBlue : undefined}>
|
||||
{i === clineModelIndex ? "❯ " : " "}
|
||||
</Text>
|
||||
<Text bold color={i === clineModelIndex ? COLORS.primaryBlue : "white"}>
|
||||
{model.name}
|
||||
</Text>
|
||||
{model.label && (
|
||||
<>
|
||||
<Text> </Text>
|
||||
<Text
|
||||
backgroundColor={model.label === "FREE" ? "gray" : COLORS.primaryBlue}
|
||||
color="black">
|
||||
{" "}
|
||||
{model.label}{" "}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<Box paddingLeft={2}>
|
||||
<Text color="gray">{model.description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{/* Browse all option */}
|
||||
<Box>
|
||||
<Text color={clineModelIndex === allModels.length ? COLORS.primaryBlue : "gray"}>
|
||||
{clineModelIndex === allModels.length ? "❯ " : " "}
|
||||
Browse all models...
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Text> </Text>
|
||||
<Text color="gray">Arrows to navigate, Enter to select</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -786,18 +852,6 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
/>
|
||||
)
|
||||
|
||||
case "bedrock_custom":
|
||||
return (
|
||||
<BedrockCustomModelFlow
|
||||
isActive={step === "bedrock_custom"}
|
||||
onCancel={() => setStep("modelid")}
|
||||
onComplete={(arn, baseModelId) => {
|
||||
setStep("saving")
|
||||
saveCustomBedrockConfiguration(arn, baseModelId)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
case "import":
|
||||
if (!importSource) {
|
||||
return null
|
||||
@@ -827,13 +881,11 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
const [menuIndex, setMenuIndex] = useState(0)
|
||||
|
||||
// Steps that allow going back with escape (apikey handled by ApiKeyInput component)
|
||||
// OcaEmployeeCheck handles its own escape key, so oca_employee_check is not in this list
|
||||
const canGoBack = [
|
||||
"provider",
|
||||
"modelid",
|
||||
"baseurl",
|
||||
"cline_auth",
|
||||
"oca_auth",
|
||||
"cline_model",
|
||||
"openai_codex_auth",
|
||||
"bedrock",
|
||||
@@ -853,7 +905,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 (isEnterKey(input, key)) {
|
||||
} else if (key.return) {
|
||||
handleMainMenuSelect(mainMenuItems[menuIndex].value)
|
||||
}
|
||||
} else if (step === "provider") {
|
||||
@@ -861,7 +913,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 (isEnterKey(input, key)) {
|
||||
} else if (key.return) {
|
||||
if (providerItems[providerIndex]) {
|
||||
handleProviderSelect(providerItems[providerIndex].value)
|
||||
}
|
||||
@@ -871,20 +923,20 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
setProviderSearch((prev) => prev + input)
|
||||
}
|
||||
} else if (step === "cline_model") {
|
||||
const maxIndex = getFeaturedModelMaxIndex(featuredModels)
|
||||
const allModels = featuredModels
|
||||
const maxIndex = allModels.length // includes "Browse all" option
|
||||
|
||||
if (key.upArrow) {
|
||||
setClineModelIndex((prev) => (prev > 0 ? prev - 1 : maxIndex))
|
||||
} else if (key.downArrow) {
|
||||
setClineModelIndex((prev) => (prev < maxIndex ? prev + 1 : 0))
|
||||
} else if (isEnterKey(input, key)) {
|
||||
if (isBrowseAllSelected(clineModelIndex, featuredModels)) {
|
||||
} else if (key.return) {
|
||||
if (clineModelIndex === allModels.length) {
|
||||
// "Browse all models" selected
|
||||
setStep("modelid")
|
||||
} else {
|
||||
const selectedModel = getFeaturedModelAtIndex(clineModelIndex, featuredModels)
|
||||
if (selectedModel) {
|
||||
handleClineModelSelect(selectedModel.id)
|
||||
}
|
||||
// Featured model selected
|
||||
handleClineModelSelect(allModels[clineModelIndex].id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -921,12 +973,9 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
<Text> </Text>
|
||||
{mainMenuItems.map((item, index) => (
|
||||
<Box key={item.value}>
|
||||
<Text>
|
||||
<Text color={index === menuIndex ? COLORS.primaryBlue : undefined}>
|
||||
{index === menuIndex ? "❯ " : " "}
|
||||
{item.label}
|
||||
</Text>
|
||||
{item.value === "cline_auth" && <Text color="yellow"> (try Opus 4.6!)</Text>}
|
||||
<Text color={index === menuIndex ? COLORS.primaryBlue : undefined}>
|
||||
{index === menuIndex ? "❯ " : " "}
|
||||
{item.label}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
@@ -73,16 +73,13 @@ const CredentialInput: React.FC<{
|
||||
|
||||
const displayValue = isPassword && value ? "•".repeat(value.length) : value
|
||||
|
||||
// Combine hint and placeholder into description shown above input
|
||||
const description = hint || (placeholder ? `e.g. ${placeholder}` : undefined)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color="white">{label}</Text>
|
||||
{description && <Text color="gray">{description}</Text>}
|
||||
{hint && <Text color="gray">{hint}</Text>}
|
||||
<Text> </Text>
|
||||
<Box>
|
||||
<Text color="white">{displayValue}</Text>
|
||||
<Text color="white">{displayValue || placeholder || ""}</Text>
|
||||
<Text inverse> </Text>
|
||||
</Box>
|
||||
<Text> </Text>
|
||||
@@ -114,11 +111,8 @@ export const BedrockSetup: React.FC<BedrockSetupProps> = ({ isActive, onComplete
|
||||
|
||||
// Filtered regions
|
||||
const filteredRegions = useMemo(() => {
|
||||
const search = regionSearch.toLowerCase().trim()
|
||||
if (!search) {
|
||||
return AWS_REGIONS
|
||||
}
|
||||
return AWS_REGIONS.filter((r) => r.toLowerCase().includes(search))
|
||||
const search = regionSearch.toLowerCase()
|
||||
return search ? AWS_REGIONS.filter((r) => r.includes(search)) : AWS_REGIONS
|
||||
}, [regionSearch])
|
||||
|
||||
const {
|
||||
@@ -173,18 +167,10 @@ export const BedrockSetup: React.FC<BedrockSetupProps> = ({ isActive, onComplete
|
||||
}
|
||||
}, [step, authMethod, onCancel])
|
||||
|
||||
const getSelectedRegion = useCallback(() => {
|
||||
if (filteredRegions.length > 0 && regionIndex >= 0 && regionIndex < filteredRegions.length) {
|
||||
return filteredRegions[regionIndex]
|
||||
}
|
||||
// If no matches, use the search term as custom region
|
||||
return regionSearch.trim() || "us-east-1"
|
||||
}, [filteredRegions, regionIndex, regionSearch])
|
||||
|
||||
const finish = useCallback(() => {
|
||||
const config: BedrockConfig = {
|
||||
awsAuthentication: authMethod === "default" ? "credentials" : authMethod,
|
||||
awsRegion: getSelectedRegion(),
|
||||
awsRegion: filteredRegions[regionIndex] || "us-east-1",
|
||||
awsUseCrossRegionInference: crossRegion,
|
||||
}
|
||||
if (authMethod === "profile") {
|
||||
@@ -195,7 +181,7 @@ export const BedrockSetup: React.FC<BedrockSetupProps> = ({ isActive, onComplete
|
||||
if (sessionToken) config.awsSessionToken = sessionToken
|
||||
}
|
||||
onComplete(config)
|
||||
}, [authMethod, profileName, accessKey, secretKey, sessionToken, getSelectedRegion, crossRegion, onComplete])
|
||||
}, [authMethod, profileName, accessKey, secretKey, sessionToken, filteredRegions, regionIndex, crossRegion, onComplete])
|
||||
|
||||
// Handle input for auth_method, region, and options steps
|
||||
useInput(
|
||||
@@ -215,11 +201,11 @@ export const BedrockSetup: React.FC<BedrockSetupProps> = ({ isActive, onComplete
|
||||
} else if (step === "region") {
|
||||
if (key.escape) {
|
||||
goBack()
|
||||
} else if (key.upArrow && filteredRegions.length > 0) {
|
||||
} else if (key.upArrow) {
|
||||
setRegionIndex((prev) => (prev > 0 ? prev - 1 : filteredRegions.length - 1))
|
||||
} else if (key.downArrow && filteredRegions.length > 0) {
|
||||
} else if (key.downArrow) {
|
||||
setRegionIndex((prev) => (prev < filteredRegions.length - 1 ? prev + 1 : 0))
|
||||
} else if (key.return && (filteredRegions.length > 0 || regionSearch.trim())) {
|
||||
} else if (key.return && filteredRegions.length > 0) {
|
||||
setStep("options")
|
||||
} else if (key.backspace || key.delete) {
|
||||
setRegionSearch((prev) => prev.slice(0, -1))
|
||||
@@ -231,17 +217,14 @@ export const BedrockSetup: React.FC<BedrockSetupProps> = ({ isActive, onComplete
|
||||
} else if (step === "options") {
|
||||
if (key.escape) {
|
||||
goBack()
|
||||
} else if (key.tab || key.return || input === " ") {
|
||||
// Tab/Enter/Space on checkbox toggles it, on Done button finishes
|
||||
if (optionIndex === 0) {
|
||||
setCrossRegion((prev) => !prev)
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
} else if (key.return) {
|
||||
finish()
|
||||
} else if (input === " ") {
|
||||
if (optionIndex === 0) setCrossRegion((prev) => !prev)
|
||||
} else if (key.upArrow) {
|
||||
setOptionIndex((prev) => (prev > 0 ? prev - 1 : 1))
|
||||
setOptionIndex(0)
|
||||
} else if (key.downArrow) {
|
||||
setOptionIndex((prev) => (prev < 1 ? prev + 1 : 0))
|
||||
setOptionIndex(0)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -259,7 +242,7 @@ export const BedrockSetup: React.FC<BedrockSetupProps> = ({ isActive, onComplete
|
||||
{i === authMethodIndex ? "❯ " : " "}
|
||||
{method.label}
|
||||
</Text>
|
||||
<Box paddingLeft={2}>
|
||||
<Box paddingLeft={4}>
|
||||
<Text color="gray">{method.description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -341,7 +324,7 @@ export const BedrockSetup: React.FC<BedrockSetupProps> = ({ isActive, onComplete
|
||||
<Text color="white">AWS Region</Text>
|
||||
<Text> </Text>
|
||||
<Box>
|
||||
<Text color="gray">Search or enter custom region: </Text>
|
||||
<Text color="gray">Search: </Text>
|
||||
<Text color="white">{regionSearch}</Text>
|
||||
<Text inverse> </Text>
|
||||
</Box>
|
||||
@@ -361,6 +344,7 @@ export const BedrockSetup: React.FC<BedrockSetupProps> = ({ isActive, onComplete
|
||||
{showRegionBottom && (
|
||||
<Text color="gray">... {filteredRegions.length - regionVisibleStart - regionVisibleCount} more below</Text>
|
||||
)}
|
||||
{filteredRegions.length === 0 && <Text color="gray">No regions match "{regionSearch}"</Text>}
|
||||
<Text> </Text>
|
||||
<Text color="gray">Type to search, arrows to navigate, Enter to select, Esc to go back</Text>
|
||||
</Box>
|
||||
@@ -377,12 +361,7 @@ export const BedrockSetup: React.FC<BedrockSetupProps> = ({ isActive, onComplete
|
||||
{crossRegion ? "[x]" : "[ ]"} Use cross-region inference
|
||||
</Text>
|
||||
<Text> </Text>
|
||||
<Text color={optionIndex === 1 ? COLORS.primaryBlue : undefined}>
|
||||
{optionIndex === 1 ? "❯ " : " "}
|
||||
Done
|
||||
</Text>
|
||||
<Text> </Text>
|
||||
<Text color="gray">Arrows to navigate, Enter to select, Esc to go back</Text>
|
||||
<Text color="gray">Space to toggle, Enter to continue, Esc to go back</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -6,185 +6,71 @@
|
||||
* - ⎿ for tool results (indented)
|
||||
*/
|
||||
|
||||
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@shared/ClineAccount"
|
||||
import { COMMAND_OUTPUT_STRING } from "@shared/combineCommandSequences"
|
||||
import type { ClineAskUseMcpServer, ClineMessage } from "@shared/ExtensionMessage"
|
||||
import type { 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"
|
||||
import { jsonParseSafe } from "../utils/parser"
|
||||
import { getToolDescription, isFileEditTool, parseToolFromMessage } from "../utils/tools"
|
||||
import { DiffView } from "./DiffView"
|
||||
import { SubagentMessage } from "./SubagentMessage"
|
||||
|
||||
/**
|
||||
* Add "(Tab)" hint after "Act mode" mentions in plain text.
|
||||
* Case-insensitive, avoids double-adding if already present.
|
||||
* Render inline markdown: **bold**, *italic*, `code`
|
||||
* Returns array of React nodes with appropriate styling
|
||||
*/
|
||||
function addActModeHint(text: string, keyPrefix: string): React.ReactNode[] {
|
||||
const actModeRegex = /\bact\s+mode\b(?!\s*\(tab\))/gi
|
||||
const parts = text.split(actModeRegex)
|
||||
const matches = text.match(actModeRegex)
|
||||
|
||||
if (!matches || parts.length <= 1) {
|
||||
return [text]
|
||||
}
|
||||
|
||||
function renderInlineMarkdown(text: string): React.ReactNode[] {
|
||||
const nodes: React.ReactNode[] = []
|
||||
parts.forEach((part, i) => {
|
||||
if (part) nodes.push(part)
|
||||
if (matches[i]) {
|
||||
// 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
|
||||
if (match.index > lastIndex) {
|
||||
nodes.push(text.slice(lastIndex, match.index))
|
||||
}
|
||||
|
||||
const fullMatch = match[0]
|
||||
const key = `md-${match.index}`
|
||||
|
||||
if (fullMatch.startsWith("**") && fullMatch.endsWith("**")) {
|
||||
// Bold
|
||||
nodes.push(
|
||||
<React.Fragment key={`${keyPrefix}-act-mode-${i}`}>
|
||||
{matches[i]}
|
||||
<Text color="gray"> (Tab)</Text>
|
||||
</React.Fragment>,
|
||||
<Text bold key={key}>
|
||||
{fullMatch.slice(2, -2)}
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
})
|
||||
return nodes
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 renderTokens(tokens: Token[], color?: string): React.ReactNode[] {
|
||||
return tokens.map((token, i) => renderToken(token, i, color))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>
|
||||
} 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>)
|
||||
}
|
||||
|
||||
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
|
||||
lastIndex = regex.lastIndex
|
||||
}
|
||||
|
||||
// Add remaining text
|
||||
if (lastIndex < text.length) {
|
||||
nodes.push(text.slice(lastIndex))
|
||||
}
|
||||
|
||||
return nodes.length > 0 ? nodes : [text]
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Render text with inline markdown support
|
||||
*/
|
||||
const MarkdownText: React.FC<{ children: string; color?: string }> = ({ children, color }) => {
|
||||
const tokens = lexer(children)
|
||||
return <Box flexDirection="column">{renderTokens(tokens, color)}</Box>
|
||||
const nodes = renderInlineMarkdown(children)
|
||||
return <Text color={color}>{nodes}</Text>
|
||||
}
|
||||
|
||||
interface ChatMessageProps {
|
||||
@@ -200,20 +86,10 @@ interface ChatMessageProps {
|
||||
* For this to work properly, parent containers must have width="100%"
|
||||
* so flexGrow={1} on the content box has a reference width to fill.
|
||||
*/
|
||||
const DotRow: React.FC<{ children: React.ReactNode; color?: string; flashing?: boolean }> = ({
|
||||
children,
|
||||
color,
|
||||
flashing = false,
|
||||
}) => (
|
||||
const DotRow: React.FC<{ children: React.ReactNode; color?: string }> = ({ children, color }) => (
|
||||
<Box flexDirection="row">
|
||||
<Box width={2}>
|
||||
{flashing ? (
|
||||
<Text color={color}>
|
||||
<Spinner type="toggle8" />
|
||||
</Text>
|
||||
) : (
|
||||
<Text color={color}>⏺</Text>
|
||||
)}
|
||||
<Text color={color}>⏺</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>{children}</Box>
|
||||
</Box>
|
||||
@@ -236,11 +112,6 @@ const ResultRow: React.FC<{ children: React.ReactNode; isFirst?: boolean }> = ({
|
||||
* Get the primary argument to display for a tool (file path, command, url, etc.)
|
||||
*/
|
||||
function getToolMainArg(_toolName: string, args: Record<string, unknown>): string {
|
||||
// Search files: show 'regex' in path
|
||||
if (typeof args.regex === "string" && typeof args.path === "string") {
|
||||
return `'${args.regex}' in ${args.path}`
|
||||
}
|
||||
|
||||
// File path
|
||||
if (typeof args.path === "string") return args.path
|
||||
if (typeof args.file_path === "string") return args.file_path
|
||||
@@ -250,6 +121,9 @@ function getToolMainArg(_toolName: string, args: Record<string, unknown>): strin
|
||||
return args.command.length > 120 ? args.command.substring(0, 117) + "..." : args.command
|
||||
}
|
||||
|
||||
// Search regex
|
||||
if (typeof args.regex === "string") return args.regex
|
||||
|
||||
// URL
|
||||
if (typeof args.url === "string") return args.url
|
||||
|
||||
@@ -297,7 +171,7 @@ function truncate(text: string, maxLength: number): string {
|
||||
/**
|
||||
* Format tool result for display
|
||||
*/
|
||||
function formatToolResult(result: string, maxLines = 5): string[] {
|
||||
function formatToolResult(result: string, maxLines: number = 5): string[] {
|
||||
const lines = result.split("\n")
|
||||
if (lines.length <= maxLines) {
|
||||
return lines
|
||||
@@ -307,31 +181,20 @@ function formatToolResult(result: string, maxLines = 5): string[] {
|
||||
return displayLines
|
||||
}
|
||||
|
||||
export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStreaming }) => {
|
||||
const { type, ask, say, text, partial } = message
|
||||
export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode }) => {
|
||||
const { type, ask, say, text } = message
|
||||
const toolColor = mode === "plan" ? "yellow" : COLORS.primaryBlue
|
||||
const { columns: terminalWidth } = useTerminalSize()
|
||||
|
||||
// User messages (task, user_feedback)
|
||||
// If multi-line, extend background to full width for consistent appearance
|
||||
if (say === "task" || say === "user_feedback") {
|
||||
const content = "> " + (text || "")
|
||||
const isMultiLine = content.includes("\n") || content.length > terminalWidth
|
||||
|
||||
if (isMultiLine) {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<Box backgroundColor="blackBright" paddingX={1} width="100%">
|
||||
<Text color="white">{content}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box backgroundColor="blackBright" paddingX={1}>
|
||||
<Text color="white">{content}</Text>
|
||||
<Box backgroundColor="blackBright" paddingRight={1}>
|
||||
<Text color="white" dimColor>
|
||||
{" "}
|
||||
>{" "}
|
||||
</Text>
|
||||
<Text color="white">{text}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
@@ -346,7 +209,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow>
|
||||
<MarkdownText>{text}</MarkdownText>
|
||||
<Text>{text}</Text>
|
||||
</DotRow>
|
||||
</Box>
|
||||
)
|
||||
@@ -364,26 +227,27 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
|
||||
if (isFileEditTool(toolInfo.toolName) && filePath && toolInfo.args.content) {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
|
||||
<DotRow color={toolColor}>
|
||||
<ToolCallText args={toolInfo.args} isAsk={isToolAsk} mode={mode} toolName={toolInfo.toolName} />
|
||||
</DotRow>
|
||||
<Box marginLeft={2}>
|
||||
<DiffView content={toolInfo.args.content as string} filePath={filePath as string | undefined} />
|
||||
<DiffView content={toolInfo.args.content} />
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Show result content for completed tools (both say and ask), or file path for pending asks
|
||||
const contentLines = toolInfo.result?.trim()
|
||||
? formatToolResult(toolInfo.result, 5)
|
||||
: (isToolAsk || isToolSay) && filePath
|
||||
? [filePath as string]
|
||||
: []
|
||||
// Show result content for completed tools, or file path for pending asks
|
||||
const contentLines =
|
||||
isToolSay && toolInfo.result?.trim()
|
||||
? formatToolResult(toolInfo.result, 5)
|
||||
: isToolAsk && filePath
|
||||
? [filePath as string]
|
||||
: []
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
|
||||
<DotRow color={toolColor}>
|
||||
<ToolCallText args={toolInfo.args} isAsk={isToolAsk} mode={mode} toolName={toolInfo.toolName} />
|
||||
</DotRow>
|
||||
{contentLines.length > 0 && (
|
||||
@@ -402,7 +266,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
|
||||
if (isToolSay) {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
|
||||
<DotRow color={toolColor}>
|
||||
<Text color={toolColor}>{truncate(text, 100)}</Text>
|
||||
</DotRow>
|
||||
</Box>
|
||||
@@ -424,7 +288,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
|
||||
<DotRow color={toolColor}>
|
||||
<Text>
|
||||
<Text color={toolColor}>{label}</Text>
|
||||
<Text>{truncate(command, 120)}</Text>
|
||||
@@ -459,102 +323,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
|
||||
)
|
||||
}
|
||||
|
||||
// MCP approval (ask) or acknowledgment (say)
|
||||
if ((type === "ask" && ask === "use_mcp_server") || say === "use_mcp_server") {
|
||||
const isAsk = type === "ask"
|
||||
const parsed = text
|
||||
? jsonParseSafe<Partial<ClineAskUseMcpServer> & { serverName: string }>(text, {
|
||||
type: undefined,
|
||||
serverName: "unknown server",
|
||||
toolName: undefined,
|
||||
arguments: undefined,
|
||||
uri: undefined,
|
||||
})
|
||||
: undefined
|
||||
|
||||
const serverName = parsed?.serverName || "unknown server"
|
||||
const actionLabel = isAsk ? "Cline wants to use MCP" : "Cline used MCP"
|
||||
const targetLine =
|
||||
parsed?.type === "access_mcp_resource"
|
||||
? `resource: ${parsed?.uri || "unknown"}`
|
||||
: parsed?.type === "use_mcp_tool"
|
||||
? `tool: ${parsed?.toolName || "unknown"}`
|
||||
: "tool: unknown"
|
||||
|
||||
let argsLines: string[] = []
|
||||
if (parsed?.arguments && parsed.arguments.trim() && parsed.arguments !== "{}") {
|
||||
let formattedArgs = parsed.arguments
|
||||
try {
|
||||
formattedArgs = JSON.stringify(JSON.parse(parsed.arguments), null, 2)
|
||||
} catch {
|
||||
// Keep raw string if not valid JSON
|
||||
}
|
||||
argsLines = formatToolResult(formattedArgs, 10)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
|
||||
<Text>
|
||||
<Text color={toolColor}>{actionLabel}</Text>
|
||||
<Text>{`: ${serverName}`}</Text>
|
||||
</Text>
|
||||
</DotRow>
|
||||
<Box flexDirection="column" marginLeft={2} width="100%">
|
||||
<ResultRow isFirst>
|
||||
<Text color="gray">{targetLine}</Text>
|
||||
</ResultRow>
|
||||
{argsLines.length > 0 && (
|
||||
<Box flexDirection="column" paddingLeft={3} width="100%">
|
||||
<Text color="gray">args:</Text>
|
||||
{argsLines.map((line, idx) => (
|
||||
<Text color="gray" key={`mcp-args-${idx}`}>
|
||||
{line}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if ((type === "ask" && ask === "use_subagents") || say === "use_subagents" || say === "subagent") {
|
||||
return <SubagentMessage isStreaming={isStreaming} message={message} mode={mode} />
|
||||
}
|
||||
|
||||
// MCP response
|
||||
if (say === "mcp_server_response" && text) {
|
||||
const lines = formatToolResult(text, 8)
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
|
||||
<Text color={toolColor}>MCP response</Text>
|
||||
</DotRow>
|
||||
<Box flexDirection="column" marginLeft={2} width="100%">
|
||||
{lines.map((line, idx) => (
|
||||
<ResultRow isFirst={idx === 0} key={idx}>
|
||||
<Text color="gray">{line}</Text>
|
||||
</ResultRow>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Error messages
|
||||
if (say === "clineignore_error") {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color="red">
|
||||
<Text color="red" wrap="wrap">
|
||||
Cline tried to access <Text bold>{text}</Text> which is blocked by the .clineignore file.
|
||||
</Text>
|
||||
</DotRow>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (say === "error" || (type === "ask" && ask === "api_req_failed")) {
|
||||
// Try to parse error message if it's JSON
|
||||
let errorMessage = text || "Unknown error"
|
||||
@@ -564,10 +333,6 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
|
||||
errorMessage = parsed.message
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Cline auth error to show sign-in instructions
|
||||
const isClineAuthError = errorMessage.includes(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color="red">
|
||||
@@ -575,13 +340,6 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
|
||||
<Text bold>Error</Text>: {errorMessage}
|
||||
</Text>
|
||||
</DotRow>
|
||||
{isClineAuthError && (
|
||||
<Box marginLeft={2} marginTop={1}>
|
||||
<Text color="gray">
|
||||
Run <Text color="cyan">/settings</Text> and go to Account to sign in.
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -669,7 +427,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
|
||||
if (say === "browser_action" || say === "browser_action_launch") {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
|
||||
<DotRow color={toolColor}>
|
||||
<Text>
|
||||
<Text color={toolColor}>Cline used the browser</Text>
|
||||
{text && (
|
||||
@@ -688,7 +446,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
|
||||
if (say === "mcp_server_request_started") {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
|
||||
<DotRow color={toolColor}>
|
||||
<Text>
|
||||
<Text color={toolColor}>Cline is using an MCP tool</Text>
|
||||
{text && (
|
||||
@@ -703,20 +461,6 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
|
||||
)
|
||||
}
|
||||
|
||||
// MCP notifications
|
||||
if (say === "mcp_notification" && text) {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor}>
|
||||
<Text>
|
||||
<Text color={toolColor}>MCP Notification</Text>
|
||||
<Text>: {truncate(text, 120)}</Text>
|
||||
</Text>
|
||||
</DotRow>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Info messages
|
||||
if (say === "info") {
|
||||
return (
|
||||
@@ -758,17 +502,6 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
|
||||
}
|
||||
}
|
||||
|
||||
// Act mode response (non-blocking progress update)
|
||||
if (type === "ask" && ask === "act_mode_respond" && text) {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor}>
|
||||
<MarkdownText color={toolColor}>{text}</MarkdownText>
|
||||
</DotRow>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Plan mode response
|
||||
if (type === "ask" && ask === "plan_mode_respond" && text) {
|
||||
const parsed = jsonParseSafe(text, { response: undefined as string | undefined })
|
||||
@@ -783,83 +516,6 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
|
||||
}
|
||||
}
|
||||
|
||||
// Mistake limit reached (ask)
|
||||
if (type === "ask" && ask === "mistake_limit_reached") {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color="red">
|
||||
<Text color="red" wrap="wrap">
|
||||
<Text bold>Error</Text>: {text || "Mistake limit reached."}
|
||||
</Text>
|
||||
</DotRow>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// New task request from assistant
|
||||
if (type === "ask" && ask === "new_task" && text) {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={COLORS.primaryBlue}>
|
||||
<Text bold color={COLORS.primaryBlue}>
|
||||
Cline wants to start a new task:
|
||||
</Text>
|
||||
</DotRow>
|
||||
<Box flexDirection="column" paddingLeft={2}>
|
||||
<Text color="gray">{text}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Condense conversation request
|
||||
if (type === "ask" && ask === "condense" && 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 condense your conversation:
|
||||
</Text>
|
||||
</DotRow>
|
||||
<Box flexDirection="column" paddingLeft={2}>
|
||||
<Text color="gray">{text}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Summarize task request
|
||||
if (type === "ask" && ask === "summarize_task" && 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 summarize the task:
|
||||
</Text>
|
||||
</DotRow>
|
||||
<Box flexDirection="column" paddingLeft={2}>
|
||||
<Text color="gray">{text}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -877,8 +533,6 @@ export const ChatMessageList: React.FC<ChatMessageListProps> = ({ messages, maxM
|
||||
const displayMessages = messages.filter((m) => {
|
||||
// Skip api_req_finished, they're just markers
|
||||
if (m.say === "api_req_finished") return false
|
||||
// Skip hidden aggregated usage messages
|
||||
if (m.say === "subagent_usage") return false
|
||||
// Skip empty text messages
|
||||
if (m.say === "text" && !m.text?.trim()) return false
|
||||
// Skip checkpoint messages
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Tests for ChatView component exit and cleanup behavior
|
||||
*
|
||||
* These tests verify that when the user exits (via shutdown event or other means),
|
||||
* These tests verify that when the user exits (via Ctrl+C or other means),
|
||||
* the input field is properly hidden before the app terminates.
|
||||
*/
|
||||
|
||||
@@ -12,7 +12,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { ChatView } from "./ChatView"
|
||||
|
||||
// Helper to wait for async state updates
|
||||
const delay = (ms = 60) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
// Using 60ms since handleExit has a 50ms setTimeout
|
||||
const delay = (ms: number = 60) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
// Type for our exit mock function
|
||||
type ExitMockFn = ReturnType<typeof vi.fn> & (() => void)
|
||||
@@ -126,16 +127,12 @@ vi.mock("../utils/file-search", () => ({
|
||||
searchWorkspaceFiles: vi.fn(async () => []),
|
||||
}))
|
||||
|
||||
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/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/input", () => ({
|
||||
isMouseEscapeSequence: vi.fn(() => false),
|
||||
@@ -178,22 +175,12 @@ vi.mock("@shared/getApiMetrics", () => ({
|
||||
totalTokensOut: 0,
|
||||
totalCost: 0,
|
||||
})),
|
||||
getLastApiReqTotalTokens: vi.fn(() => 0),
|
||||
}))
|
||||
|
||||
vi.mock("child_process", () => ({
|
||||
exec: vi.fn(),
|
||||
execFile: vi.fn(),
|
||||
execSync: vi.fn(() => "main"),
|
||||
}))
|
||||
|
||||
// Mock telemetry service to prevent HostProvider errors in shutdown handler
|
||||
vi.mock("@/services/telemetry", () => ({
|
||||
telemetryService: {
|
||||
captureHostEvent: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Helper to create a typed mock for onExit
|
||||
const createExitMock = (): ExitMockFn => vi.fn() as ExitMockFn
|
||||
|
||||
@@ -226,6 +213,33 @@ describe("ChatView Exit and Cleanup", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("Ctrl+C exit handling", () => {
|
||||
it("should hide input but keep footer, then call onExit", async () => {
|
||||
const { lastFrame, stdin } = render(<ChatView onExit={mockOnExit} />)
|
||||
|
||||
// Verify UI visible before Ctrl+C
|
||||
expect(lastFrame()).toContain("Input:")
|
||||
expect(lastFrame()).toContain("@ for files")
|
||||
|
||||
// Simulate Ctrl+C
|
||||
stdin.write("\x03")
|
||||
|
||||
// onExit should not be called immediately
|
||||
expect(mockOnExit).not.toHaveBeenCalled()
|
||||
|
||||
// Wait for state update and callback
|
||||
await delay()
|
||||
|
||||
// Input should be hidden, but footer should remain
|
||||
const frameAfter = lastFrame()
|
||||
expect(frameAfter).not.toContain("Input:")
|
||||
expect(frameAfter).toContain("@ for files")
|
||||
|
||||
// onExit should have been called
|
||||
expect(mockOnExit).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Shutdown event handling", () => {
|
||||
it("should subscribe on mount and unsubscribe on unmount", () => {
|
||||
const { unmount } = render(<ChatView onExit={mockOnExit} />)
|
||||
@@ -235,59 +249,39 @@ describe("ChatView Exit and Cleanup", () => {
|
||||
expect(shutdownMockState.listeners.length).toBe(0)
|
||||
})
|
||||
|
||||
it("should hide input when shutdown event fires", async () => {
|
||||
it("should hide UI when shutdown event fires", async () => {
|
||||
const { lastFrame } = render(<ChatView onExit={mockOnExit} />)
|
||||
|
||||
// Input should be visible initially
|
||||
expect(lastFrame()).toContain("Input:")
|
||||
|
||||
// Fire shutdown event (simulates Ctrl+C)
|
||||
shutdownMockState.fire()
|
||||
await delay()
|
||||
|
||||
// Input should be hidden after shutdown
|
||||
expect(lastFrame()).not.toContain("Input:")
|
||||
})
|
||||
|
||||
it("should preserve footer when shutdown event fires", async () => {
|
||||
const { lastFrame } = render(<ChatView onExit={mockOnExit} />)
|
||||
|
||||
// Footer should be visible initially
|
||||
expect(lastFrame()).toContain("@ for files")
|
||||
|
||||
// Fire shutdown event
|
||||
shutdownMockState.fire()
|
||||
await delay()
|
||||
|
||||
// Footer should still be present (only input is hidden)
|
||||
expect(lastFrame()).toContain("@ for files")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Edge cases", () => {
|
||||
it("should handle shutdown event when onExit prop is undefined", async () => {
|
||||
const { lastFrame } = render(<ChatView />)
|
||||
it("should handle exit when onExit prop is undefined", async () => {
|
||||
const { lastFrame, stdin } = render(<ChatView />)
|
||||
|
||||
// Fire shutdown event
|
||||
shutdownMockState.fire()
|
||||
stdin.write("\x03")
|
||||
await delay()
|
||||
|
||||
// Should not throw, UI should still hide
|
||||
expect(lastFrame()).not.toContain("Input:")
|
||||
})
|
||||
|
||||
it("should handle multiple shutdown events gracefully", async () => {
|
||||
const { lastFrame } = render(<ChatView onExit={mockOnExit} />)
|
||||
it("should handle multiple Ctrl+C presses gracefully", async () => {
|
||||
const { stdin } = render(<ChatView onExit={mockOnExit} />)
|
||||
|
||||
// Fire multiple shutdown events
|
||||
shutdownMockState.fire()
|
||||
shutdownMockState.fire()
|
||||
shutdownMockState.fire()
|
||||
stdin.write("\x03")
|
||||
stdin.write("\x03")
|
||||
stdin.write("\x03")
|
||||
|
||||
await delay()
|
||||
|
||||
// UI should still hide properly
|
||||
expect(lastFrame()).not.toContain("Input:")
|
||||
expect(mockOnExit).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -300,15 +294,14 @@ describe("ChatView UI State During Exit", () => {
|
||||
|
||||
it("should preserve static content and footer, only hide input during exit", async () => {
|
||||
const onExit = createExitMock()
|
||||
const { lastFrame } = render(<ChatView onExit={onExit} />)
|
||||
const { lastFrame, stdin } = render(<ChatView onExit={onExit} />)
|
||||
|
||||
// Footer contains auto-approve toggle
|
||||
expect(lastFrame()).toContain("Auto-approve")
|
||||
expect(lastFrame()).toContain("What can I do for you?")
|
||||
expect(lastFrame()).toContain("Input:")
|
||||
|
||||
// Fire shutdown event
|
||||
shutdownMockState.fire()
|
||||
stdin.write("\x03")
|
||||
await delay()
|
||||
|
||||
const frameAfter = lastFrame()
|
||||
@@ -101,18 +101,15 @@
|
||||
* - log-update: node_modules/ink/build/log-update.js (eraseLines logic)
|
||||
*/
|
||||
|
||||
import type { ApiProvider, ModelInfo } from "@shared/api"
|
||||
import { combineCommandSequences } from "@shared/combineCommandSequences"
|
||||
import { combineHookSequences } from "@shared/combineHookSequences"
|
||||
import type { ClineAsk, ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { getApiMetrics, getLastApiReqTotalTokens } from "@shared/getApiMetrics"
|
||||
import { getApiMetrics } from "@shared/getApiMetrics"
|
||||
import { EmptyRequest, StringRequest } from "@shared/proto/cline/common"
|
||||
import type { SlashCommandInfo } from "@shared/proto/cline/slash"
|
||||
import { getProviderDefaultModelId, getProviderModelIdKey } from "@shared/storage"
|
||||
import { CLI_ONLY_COMMANDS } from "@shared/slashCommands"
|
||||
import type { Mode } from "@shared/storage/types"
|
||||
import { execSync } from "child_process"
|
||||
import { Box, Static, Text, useApp, useInput } from "ink"
|
||||
// biome-ignore lint/style/useImportType: JSX requires React as a value (jsx: "react" in tsconfig)
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { getAvailableSlashCommands } from "@/core/controller/slash/getAvailableSlashCommands"
|
||||
import { showTaskWithId } from "@/core/controller/task/showTaskWithId"
|
||||
@@ -122,9 +119,7 @@ import { Logger } from "@/shared/services/Logger"
|
||||
import { Session } from "@/shared/services/Session"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useTaskContext, useTaskState } from "../context/TaskContext"
|
||||
import { useHomeEndKeys } from "../hooks/useHomeEndKeys"
|
||||
import { useIsSpinnerActive } from "../hooks/useStateSubscriber"
|
||||
import { findWordEnd, findWordStart, useTextInput } from "../hooks/useTextInput"
|
||||
import { moveCursorDown, moveCursorUp } from "../utils/cursor"
|
||||
import { setTerminalTitle } from "../utils/display"
|
||||
import {
|
||||
@@ -136,53 +131,26 @@ import {
|
||||
} from "../utils/file-search"
|
||||
import { isMouseEscapeSequence } from "../utils/input"
|
||||
import { jsonParseSafe, parseImagesFromInput } from "../utils/parser"
|
||||
import {
|
||||
createCliOnlySlashCommands,
|
||||
extractSlashQuery,
|
||||
filterCommands,
|
||||
getStandaloneSlashCommandToExecute,
|
||||
insertSlashCommand,
|
||||
sortCommandsWorkflowsFirst,
|
||||
} from "../utils/slash-commands"
|
||||
import { waitFor } from "../utils/timeout"
|
||||
import { extractSlashQuery, filterCommands, insertSlashCommand, sortCommandsWorkflowsFirst } from "../utils/slash-commands"
|
||||
import { isFileEditTool, parseToolFromMessage } from "../utils/tools"
|
||||
import { shutdownEvent } from "../vscode-shim"
|
||||
import { ActionButtons, type ButtonActionType, getButtonConfig, getVisibleButtons } from "./ActionButtons"
|
||||
import { AsciiMotionCli, StaticRobotFrame } from "./AsciiMotionCli"
|
||||
import { ChatMessage } from "./ChatMessage"
|
||||
import { FileMentionMenu } from "./FileMentionMenu"
|
||||
import { HelpPanelContent } from "./HelpPanelContent"
|
||||
import { HighlightedInput } from "./HighlightedInput"
|
||||
import { HistoryPanelContent } from "./HistoryPanelContent"
|
||||
import { providerModels } from "./ModelPicker"
|
||||
import { SessionSummary } from "./SessionSummary"
|
||||
import { SettingsPanelContent } from "./SettingsPanelContent"
|
||||
import { SkillsPanelContent } from "./SkillsPanelContent"
|
||||
import { SlashCommandMenu } from "./SlashCommandMenu"
|
||||
import { ThinkingIndicator } from "./ThinkingIndicator"
|
||||
|
||||
/**
|
||||
* Persistent input storage that survives React remounts (e.g., during terminal resize).
|
||||
* Keyed by a stable identifier so each task/session maintains its own input state.
|
||||
*/
|
||||
interface PersistedInputState {
|
||||
text: string
|
||||
cursorPos: number
|
||||
pastedTexts: Map<number, string>
|
||||
pasteCounter: number
|
||||
}
|
||||
|
||||
const inputStateStorage = new Map<string, PersistedInputState>()
|
||||
|
||||
function getInputStorageKey(controller: any, taskId?: string): string {
|
||||
// Use taskId if available, otherwise fall back to controller instance
|
||||
return taskId || (controller?.task?.taskId ?? "default")
|
||||
}
|
||||
|
||||
interface ChatViewProps {
|
||||
controller?: any
|
||||
onExit?: () => void
|
||||
onComplete?: () => void
|
||||
onError?: () => void
|
||||
robotTopRow?: number
|
||||
initialPrompt?: string
|
||||
initialImages?: string[]
|
||||
taskId?: string
|
||||
@@ -236,9 +204,9 @@ function getGitDiffStats(cwd?: string): GitDiffStats | null {
|
||||
const delMatch = output.match(/(\d+) deletion/)
|
||||
|
||||
return {
|
||||
files: filesMatch ? Number.parseInt(filesMatch[1], 10) : 0,
|
||||
additions: addMatch ? Number.parseInt(addMatch[1], 10) : 0,
|
||||
deletions: delMatch ? Number.parseInt(delMatch[1], 10) : 0,
|
||||
files: filesMatch ? parseInt(filesMatch[1], 10) : 0,
|
||||
additions: addMatch ? parseInt(addMatch[1], 10) : 0,
|
||||
deletions: delMatch ? parseInt(delMatch[1], 10) : 0,
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
@@ -249,7 +217,7 @@ function getGitDiffStats(cwd?: string): GitDiffStats | null {
|
||||
* Create a progress bar for context window usage
|
||||
* Returns { filled, empty } strings to allow different coloring
|
||||
*/
|
||||
function createContextBar(used: number, total: number, width = 8): { filled: string; empty: string } {
|
||||
function createContextBar(used: number, total: number, width: number = 8): { filled: string; empty: string } {
|
||||
const ratio = Math.min(used / total, 1)
|
||||
// Use ceil so any usage > 0 shows at least one bar
|
||||
const filledCount = used > 0 ? Math.max(1, Math.ceil(ratio * width)) : 0
|
||||
@@ -290,7 +258,6 @@ const YOLO_INTERACTIVE_ASKS = new Set<ClineAsk>([
|
||||
"plan_mode_respond",
|
||||
"resume_task",
|
||||
"resume_completed_task",
|
||||
"new_task",
|
||||
])
|
||||
|
||||
function isYoloSuppressed(yolo: boolean, ask: ClineAsk | undefined): boolean {
|
||||
@@ -339,7 +306,7 @@ function parseAskOptions(text: string): string[] {
|
||||
*/
|
||||
function expandPastedTexts(text: string, pastedTexts: Map<number, string>): string {
|
||||
return text.replace(/\[Pasted text #(\d+) \+\d+ lines\]/g, (match, num) => {
|
||||
const content = pastedTexts.get(Number.parseInt(num, 10))
|
||||
const content = pastedTexts.get(parseInt(num, 10))
|
||||
return content ?? match
|
||||
})
|
||||
}
|
||||
@@ -349,6 +316,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
onExit,
|
||||
onComplete: _onComplete,
|
||||
onError,
|
||||
robotTopRow,
|
||||
initialPrompt,
|
||||
initialImages,
|
||||
taskId,
|
||||
@@ -358,33 +326,15 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
|
||||
// Get task state from context
|
||||
const taskState = useTaskState()
|
||||
const { controller: taskController, clearState } = useTaskContext()
|
||||
const { controller: taskController } = useTaskContext()
|
||||
const { isActive: isSpinnerActive, startTime: spinnerStartTime } = useIsSpinnerActive()
|
||||
|
||||
// Prefer prop controller over context controller (memoized for stable reference in callbacks)
|
||||
const ctrl = useMemo(() => controller || taskController, [controller, taskController])
|
||||
|
||||
// Input state - using hook for text editing with keyboard shortcuts
|
||||
const {
|
||||
text: textInput,
|
||||
cursorPos,
|
||||
setText: setTextInput,
|
||||
setCursorPos,
|
||||
handleKeyboardSequence,
|
||||
handleCtrlShortcut,
|
||||
deleteCharBefore,
|
||||
insertText: insertTextAtCursor,
|
||||
} = useTextInput()
|
||||
|
||||
// Get storage key for persisting input across remounts
|
||||
const storageKey = useMemo(() => getInputStorageKey(ctrl, taskId), [ctrl, taskId])
|
||||
|
||||
// Refs for text input and cursor position (used by useHomeEndKeys and to avoid stale closures in useInput)
|
||||
const textInputRef = useRef(textInput)
|
||||
textInputRef.current = textInput
|
||||
const cursorPosRef = useRef(cursorPos)
|
||||
cursorPosRef.current = cursorPos
|
||||
|
||||
// Input state
|
||||
const [textInput, setTextInput] = useState("")
|
||||
const [cursorPos, setCursorPos] = useState(0)
|
||||
const [fileResults, setFileResults] = useState<FileSearchResult[]>([])
|
||||
const [selectedIndex, setSelectedIndex] = useState(0) // For file menu
|
||||
const [historyIndex, setHistoryIndex] = useState(-1) // -1 = not browsing history, 0+ = history item index
|
||||
@@ -395,10 +345,8 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
const [userScrolled, setUserScrolled] = useState(false)
|
||||
|
||||
// Pasted text storage - maps placeholder number to full pasted content
|
||||
const [pastedTexts, setPastedTexts] = useState<Map<number, string>>(() => {
|
||||
return inputStateStorage.get(storageKey)?.pastedTexts ?? new Map()
|
||||
})
|
||||
const pasteCounterRef = useRef<number>(inputStateStorage.get(storageKey)?.pasteCounter ?? 0)
|
||||
const [pastedTexts, setPastedTexts] = useState<Map<number, string>>(new Map())
|
||||
const pasteCounterRef = useRef(0)
|
||||
// Track paste timing to combine chunks that arrive in rapid succession
|
||||
const lastPasteTimeRef = useRef<number>(0)
|
||||
const activePasteNumRef = useRef<number>(0)
|
||||
@@ -409,53 +357,19 @@ 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[]>(() => createCliOnlySlashCommands())
|
||||
const [availableCommands, setAvailableCommands] = useState<SlashCommandInfo[]>([])
|
||||
const [selectedSlashIndex, setSelectedSlashIndex] = useState(0)
|
||||
const [slashMenuDismissed, setSlashMenuDismissed] = useState(false)
|
||||
const lastSlashIndexRef = useRef<number>(-1)
|
||||
|
||||
// Panel state
|
||||
// Settings panel state
|
||||
const [activePanel, setActivePanel] = useState<
|
||||
| { type: "settings"; initialMode?: "model-picker" | "featured-models"; initialModelKey?: "actModelId" | "planModelId" }
|
||||
| { type: "history" }
|
||||
| { type: "help" }
|
||||
| { type: "skills" }
|
||||
| null
|
||||
{ type: "settings"; initialMode?: "model-picker" } | { type: "history" } | null
|
||||
>(null)
|
||||
|
||||
// Handle Home/End keys from raw stdin (Ink doesn't expose these in useInput)
|
||||
useHomeEndKeys({
|
||||
onHome: useCallback(() => setCursorPos(0), [setCursorPos]),
|
||||
onEnd: useCallback(() => setCursorPos(textInputRef.current.length), [setCursorPos]),
|
||||
isActive: !activePanel, // Only active when no panel is open
|
||||
})
|
||||
|
||||
// Track when we're exiting to hide UI elements before exit
|
||||
const [isExiting, setIsExiting] = useState(false)
|
||||
|
||||
// Restore input state from storage on mount (after resize remount)
|
||||
useEffect(() => {
|
||||
const stored = inputStateStorage.get(storageKey)
|
||||
if (stored) {
|
||||
setTextInput(stored.text)
|
||||
setCursorPos(stored.cursorPos)
|
||||
setPastedTexts(stored.pastedTexts)
|
||||
pasteCounterRef.current = stored.pasteCounter
|
||||
}
|
||||
}, [storageKey, setTextInput, setCursorPos])
|
||||
|
||||
// Persist input state to storage whenever it changes (survives remount)
|
||||
useEffect(() => {
|
||||
if (textInput || pastedTexts.size > 0) {
|
||||
inputStateStorage.set(storageKey, {
|
||||
text: textInput,
|
||||
cursorPos,
|
||||
pastedTexts: new Map(pastedTexts),
|
||||
pasteCounter: pasteCounterRef.current,
|
||||
})
|
||||
}
|
||||
}, [storageKey, textInput, cursorPos, pastedTexts])
|
||||
|
||||
// Task switch handling: when switching tasks via /history, we clear the terminal and
|
||||
// increment a counter used as the root Box's key. This forces React to remount the tree,
|
||||
// giving us a fresh Static instance. Mirrors how App.tsx handles resize with resizeKey.
|
||||
@@ -482,10 +396,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
return stateManager.getGlobalSettingsKey("mode") || "act"
|
||||
})
|
||||
|
||||
const [yolo, _setYolo] = useState<boolean>(() => StateManager.get().getGlobalSettingsKey("yoloModeToggled") ?? false)
|
||||
const [autoApproveAll, setAutoApproveAll] = useState<boolean>(
|
||||
() => StateManager.get().getGlobalSettingsKey("autoApproveAllToggled") ?? false,
|
||||
)
|
||||
const [yolo, setYolo] = useState<boolean>(() => StateManager.get().getGlobalSettingsKey("yoloModeToggled") ?? false)
|
||||
|
||||
// Sync mode from core state updates (e.g. yolo auto-switching plan to act)
|
||||
useEffect(() => {
|
||||
@@ -494,68 +405,25 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}
|
||||
}, [taskState.mode])
|
||||
|
||||
const toggleAutoApproveAll = useCallback(() => {
|
||||
const newValue = !autoApproveAll
|
||||
setAutoApproveAll(newValue)
|
||||
StateManager.get().setGlobalState("autoApproveAllToggled", newValue)
|
||||
}, [autoApproveAll])
|
||||
const toggleYolo = useCallback(() => {
|
||||
const newValue = !yolo
|
||||
setYolo(newValue)
|
||||
StateManager.get().setGlobalState("yoloModeToggled", newValue)
|
||||
}, [yolo])
|
||||
|
||||
// Get provider based on current mode (computed first since modelId depends on it)
|
||||
const provider = useMemo(() => {
|
||||
const stateManager = StateManager.get()
|
||||
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
return (stateManager.getGlobalSettingsKey(providerKey) as string) || ""
|
||||
}, [mode, activePanel])
|
||||
|
||||
// Get model ID based on current mode and provider
|
||||
// Different providers use different state keys (e.g., cline uses actModeOpenRouterModelId)
|
||||
// Get model ID based on current mode
|
||||
// Re-read when activePanel changes (settings panel closes) to pick up changes
|
||||
// Falls back to provider's default model if no model has been explicitly set
|
||||
const modelId = useMemo(() => {
|
||||
if (!provider) return ""
|
||||
const stateManager = StateManager.get()
|
||||
const modelKey = getProviderModelIdKey(provider as ApiProvider, mode)
|
||||
return (stateManager.getGlobalSettingsKey(modelKey) as string) || getProviderDefaultModelId(provider as ApiProvider) || ""
|
||||
}, [mode, provider, activePanel])
|
||||
const modelKey = mode === "act" ? "actModeApiModelId" : "planModeApiModelId"
|
||||
return (stateManager.getGlobalSettingsKey(modelKey) as string) || "claude-sonnet-4-20250514"
|
||||
}, [mode, activePanel])
|
||||
|
||||
const toggleMode = useCallback(async () => {
|
||||
const newMode: Mode = mode === "act" ? "plan" : "act"
|
||||
setMode(newMode)
|
||||
|
||||
// When switching from plan to act, include any text in the input box
|
||||
// Text stays visible in the input - don't clear it
|
||||
if (newMode === "act" && textInput.trim()) {
|
||||
const expandedText = expandPastedTexts(textInput, pastedTexts)
|
||||
await ctrl.togglePlanActMode(newMode, { message: expandedText.trim() })
|
||||
} else {
|
||||
await ctrl.togglePlanActMode(newMode)
|
||||
}
|
||||
}, [mode, ctrl, textInput, pastedTexts])
|
||||
|
||||
// Clear the terminal view and reset task state (used by /clear and "Start New Task" button)
|
||||
// This is async to ensure clearTask() completes before we remount, preventing race conditions
|
||||
// where the old messages get fetched and restored before the controller clears them.
|
||||
const clearViewAndResetTask = useCallback(async () => {
|
||||
// First, clear the task on the controller and wait for it to finish
|
||||
// This ensures the controller has no messages before we remount
|
||||
if (ctrl) {
|
||||
await ctrl.clearTask()
|
||||
}
|
||||
|
||||
// Now clear the terminal and reset React state
|
||||
process.stdout.write("\x1b[2J\x1b[3J\x1b[H") // Clear screen + scrollback, cursor home
|
||||
setTaskSwitchKey((k) => k + 1) // Force remount for fresh Static instance
|
||||
clearState() // Force clear React state (bypasses empty messages check)
|
||||
setTextInput("")
|
||||
setCursorPos(0)
|
||||
// Clear persisted state
|
||||
inputStateStorage.delete(storageKey)
|
||||
|
||||
// Post the now-empty state
|
||||
if (ctrl) {
|
||||
ctrl.postStateToWebview()
|
||||
}
|
||||
}, [ctrl, clearState, storageKey])
|
||||
await ctrl.togglePlanActMode(newMode)
|
||||
}, [mode, ctrl])
|
||||
|
||||
const refs = useRef({
|
||||
searchTimeout: null as NodeJS.Timeout | null,
|
||||
@@ -620,15 +488,16 @@ 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.
|
||||
// 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)])
|
||||
// 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,
|
||||
}))
|
||||
setAvailableCommands(sortCommandsWorkflowsFirst([...cliCommands, ...cliOnlyCommands]))
|
||||
} catch {
|
||||
// Keep CLI-only commands available even if backend command loading fails.
|
||||
// Fallback: commands will be empty, menu won't show
|
||||
}
|
||||
}
|
||||
loadCommands()
|
||||
@@ -661,14 +530,11 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
if (m.say === "checkpoint_created") return false
|
||||
if (m.say === "api_req_started") return false
|
||||
if (m.say === "api_req_retried") return false // Redundant with error_retry messages
|
||||
if (m.say === "reasoning") return false // Hide thinking traces - they clutter the UI
|
||||
return true
|
||||
})
|
||||
|
||||
// Combine hook messages with their output, then command messages (like webview does)
|
||||
// CLI always has hooks enabled, so we always apply combineHookSequences
|
||||
const withHooks = combineHookSequences(filtered)
|
||||
return combineCommandSequences(withHooks)
|
||||
// Combine command messages with their output (like webview does)
|
||||
return combineCommandSequences(filtered)
|
||||
}, [messages])
|
||||
|
||||
// Detect task switches by watching first message timestamp change.
|
||||
@@ -815,8 +681,6 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
setCursorPos(0)
|
||||
setPastedTexts(new Map()) // Clear stored pastes
|
||||
pasteCounterRef.current = 0
|
||||
// Clear persisted state
|
||||
inputStateStorage.delete(storageKey)
|
||||
|
||||
try {
|
||||
await ctrl.task.handleWebviewAskResponse(responseType, expandedText)
|
||||
@@ -824,7 +688,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
// Controller may be disposed
|
||||
}
|
||||
},
|
||||
[ctrl, pendingAsk, pastedTexts, storageKey],
|
||||
[ctrl, pendingAsk, pastedTexts],
|
||||
)
|
||||
|
||||
// Handle cancel/interrupt
|
||||
@@ -848,77 +712,6 @@ 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
|
||||
@@ -938,13 +731,8 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
sendAskResponse("yesButtonClicked")
|
||||
break
|
||||
case "reject":
|
||||
// Check for states that should trigger exit (all end-of-task states with Exit button)
|
||||
if (
|
||||
pendingAsk?.ask === "resume_task" ||
|
||||
pendingAsk?.ask === "resume_completed_task" ||
|
||||
pendingAsk?.ask === "completion_result" ||
|
||||
pendingAsk?.ask === "new_task"
|
||||
) {
|
||||
// Check for resume states that should trigger exit
|
||||
if (pendingAsk?.ask === "resume_task" || pendingAsk?.ask === "resume_completed_task") {
|
||||
handleExit()
|
||||
} else {
|
||||
sendAskResponse("noButtonClicked")
|
||||
@@ -955,23 +743,17 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
sendAskResponse("yesButtonClicked")
|
||||
break
|
||||
case "new_task":
|
||||
if (pendingAsk?.ask === "new_task") {
|
||||
// Model called new_task tool - create new task with context
|
||||
setRespondedToAsk(pendingAsk.ts)
|
||||
setTextInput("")
|
||||
setCursorPos(0)
|
||||
await ctrl.initTask(pendingAsk.text || "")
|
||||
} else {
|
||||
// From completion_result or resume_completed_task - full clear
|
||||
await clearViewAndResetTask()
|
||||
}
|
||||
// For now, signal to start a new task (user can type new prompt)
|
||||
setRespondedToAsk(pendingAsk?.ts || null)
|
||||
setTextInput("")
|
||||
setCursorPos(0)
|
||||
break
|
||||
case "cancel":
|
||||
handleCancel()
|
||||
break
|
||||
}
|
||||
},
|
||||
[controller, taskController, sendAskResponse, pendingAsk, handleExit, handleCancel, clearViewAndResetTask],
|
||||
[controller, taskController, sendAskResponse, pendingAsk, handleExit, handleCancel],
|
||||
)
|
||||
|
||||
// Handle task submission (new task)
|
||||
@@ -986,8 +768,6 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
setCursorPos(0)
|
||||
setPastedTexts(new Map()) // Clear stored pastes
|
||||
pasteCounterRef.current = 0
|
||||
// Clear persisted state
|
||||
inputStateStorage.delete(storageKey)
|
||||
|
||||
try {
|
||||
// Convert image paths to data URLs if needed
|
||||
@@ -1015,12 +795,10 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
onError?.()
|
||||
}
|
||||
},
|
||||
[ctrl, onError, pastedTexts, storageKey],
|
||||
[ctrl, onError, pastedTexts],
|
||||
)
|
||||
|
||||
// Auto-submit initial prompt if provided
|
||||
// When taskId is also provided, this sends the prompt to resume the existing task
|
||||
// When no taskId, this creates a new task with the prompt
|
||||
useEffect(() => {
|
||||
const autoSubmit = async () => {
|
||||
if (!initialPrompt && (!initialImages || initialImages.length === 0)) {
|
||||
@@ -1041,32 +819,8 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
if (initialPrompt) {
|
||||
setTerminalTitle(initialPrompt)
|
||||
}
|
||||
|
||||
if (taskId) {
|
||||
// Resuming an existing task with a prompt - wait for task to load first
|
||||
// The task loading happens in the other useEffect via showTaskWithId
|
||||
// We need to wait for it to complete before sending the resume message
|
||||
const task = await waitFor(() => ctrl.task, 5000)
|
||||
|
||||
if (task) {
|
||||
// Send the prompt as a message to resume the task
|
||||
await task.handleWebviewAskResponse("messageResponse", initialPrompt || "")
|
||||
} else {
|
||||
// Task failed to load, fall back to creating new task
|
||||
Logger.error(`Failed to load task ${taskId} for resume, creating new task instead`)
|
||||
await ctrl.initTask(
|
||||
initialPrompt || "",
|
||||
initialImages && initialImages.length > 0 ? initialImages : undefined,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// New task - use initTask
|
||||
// initialImages are already data URLs from index.ts processing
|
||||
await ctrl.initTask(
|
||||
initialPrompt || "",
|
||||
initialImages && initialImages.length > 0 ? initialImages : undefined,
|
||||
)
|
||||
}
|
||||
// initialImages are already data URLs from index.ts processing
|
||||
await ctrl.initTask(initialPrompt || "", initialImages && initialImages.length > 0 ? initialImages : undefined)
|
||||
} catch (_error) {
|
||||
onError?.()
|
||||
}
|
||||
@@ -1130,67 +884,27 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}, [mentionInfo.inMentionMode, mentionInfo.query, workspacePath])
|
||||
|
||||
// Handle keyboard input
|
||||
//
|
||||
// KEYBOARD PRIORITY ORDER (first match wins):
|
||||
// 1. Mouse escape sequences -> filtered out (from AsciiMotionCli tracking)
|
||||
// 2. Option+arrow escape sequences -> word navigation (handleKeyboardSequence)
|
||||
// 3. Option+arrow via key.meta -> word navigation (backup for when Ink parses it)
|
||||
// 4. Panel open -> bail (let panel handle its own input)
|
||||
// 5. Slash menu open -> menu navigation (up/down/tab/return/escape)
|
||||
// 6. File menu open -> menu navigation (up/down/tab/return/escape)
|
||||
// 7. History navigation -> up/down when input empty or matches history item
|
||||
// 8. Button actions -> "1"/"2" keys when buttons shown and no text typed
|
||||
// 9. Ask responses -> return to send, numbers for option selection
|
||||
// 10. Ctrl shortcuts -> Ctrl+A/E/W/U (handleCtrlShortcut)
|
||||
// 11. Large paste detection -> collapse into placeholder
|
||||
// 12. Normal input -> tab (mode toggle), return (submit), backspace, arrows, text
|
||||
//
|
||||
// Note: Home/End keys are handled separately by useHomeEndKeys hook because
|
||||
// Ink doesn't expose them in useInput (it sets input='' for these keys).
|
||||
//
|
||||
useInput((input, key) => {
|
||||
// 1. Filter out mouse escape sequences from AsciiMotionCli's mouse tracking
|
||||
// Handle Ctrl+C - hide input and exit gracefully
|
||||
if (input === "\x03" || (key.ctrl && input === "c")) {
|
||||
handleExit()
|
||||
return
|
||||
}
|
||||
|
||||
// Filter out mouse escape sequences from AsciiMotionCli's mouse tracking
|
||||
if (isMouseEscapeSequence(input)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Handle Option+arrow escape sequences for word navigation
|
||||
if (handleKeyboardSequence(input)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Handle Option+arrow via key.meta (backup - Ink sometimes parses these instead of passing raw sequence)
|
||||
if (key.meta) {
|
||||
if (key.leftArrow) {
|
||||
setCursorPos(findWordStart(textInputRef.current, cursorPosRef.current))
|
||||
return
|
||||
}
|
||||
if (key.rightArrow) {
|
||||
setCursorPos(findWordEnd(textInputRef.current, cursorPosRef.current))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 4. When a panel is open, let the panel handle its own input
|
||||
// When a panel is open, let the panel handle its own input
|
||||
if (activePanel) {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
// Slash command menu navigation (takes priority over file menu)
|
||||
if (inSlashMenu) {
|
||||
if (key.upArrow) {
|
||||
setSelectedSlashIndex((i) => Math.max(0, i - 1))
|
||||
@@ -1203,7 +917,35 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
if (key.tab || key.return) {
|
||||
const cmd = filteredCommands[selectedSlashIndex]
|
||||
if (cmd) {
|
||||
if (handleCliOnlySlashCommand(cmd.name)) {
|
||||
// Handle CLI-only commands locally
|
||||
if (cmd.name === "settings") {
|
||||
setActivePanel({ type: "settings" })
|
||||
setTextInput("")
|
||||
setCursorPos(0)
|
||||
setSelectedSlashIndex(0)
|
||||
setSlashMenuDismissed(true)
|
||||
return
|
||||
}
|
||||
if (cmd.name === "models") {
|
||||
// If separate models for plan/act is enabled, just open settings (user picks which mode)
|
||||
const hasSeparateModels = StateManager.get().getGlobalSettingsKey("planActSeparateModelsSetting")
|
||||
setActivePanel({ type: "settings", initialMode: hasSeparateModels ? undefined : "model-picker" })
|
||||
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 === "exit") {
|
||||
handleExit()
|
||||
return
|
||||
}
|
||||
const newText = insertSlashCommand(textInput, slashInfo.slashIndex, cmd.name)
|
||||
@@ -1221,7 +963,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// 6. File mention menu navigation
|
||||
// File mention menu navigation
|
||||
if (inFileMenu) {
|
||||
if (key.upArrow) {
|
||||
setSelectedIndex((i) => Math.max(0, i - 1))
|
||||
@@ -1249,7 +991,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// 7. History navigation with up/down arrows
|
||||
// History navigation with up/down arrows
|
||||
// Only works when: input is empty, or input matches the currently selected history item
|
||||
if (key.upArrow && !inSlashMenu && !inFileMenu) {
|
||||
const historyItems = getHistoryItems()
|
||||
@@ -1299,7 +1041,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Handle button actions (1 for primary, 2 for secondary)
|
||||
// Handle button actions (1 for primary, 2 for secondary)
|
||||
// Only when buttons are enabled, not streaming, and no text has been typed
|
||||
if (
|
||||
buttonConfig.enableButtons &&
|
||||
@@ -1314,8 +1056,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
if (hasPrimary && buttonConfig.primaryAction) {
|
||||
handleButtonAction(buttonConfig.primaryAction, true)
|
||||
return
|
||||
}
|
||||
if (hasSecondary && !hasPrimary && buttonConfig.secondaryAction) {
|
||||
} else if (hasSecondary && !hasPrimary && buttonConfig.secondaryAction) {
|
||||
handleButtonAction(buttonConfig.secondaryAction, false)
|
||||
return
|
||||
}
|
||||
@@ -1327,7 +1068,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// 9. Handle ask responses for options and text input
|
||||
// Handle ask responses for options and text input
|
||||
if (pendingAsk && !isYoloSuppressed(yolo, pendingAsk.ask as ClineAsk | undefined)) {
|
||||
// Allow sending text message for any ask type where sending is enabled
|
||||
if (key.return && textInput.trim() && !buttonConfig.sendingDisabled) {
|
||||
@@ -1336,7 +1077,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}
|
||||
// Number selection for options (only when no text typed yet)
|
||||
if (askType === "options") {
|
||||
const num = Number.parseInt(input, 10)
|
||||
const num = parseInt(input, 10)
|
||||
if (textInput === "" && !Number.isNaN(num) && num >= 1 && num <= askOptions.length) {
|
||||
const selectedOption = askOptions[num - 1]
|
||||
sendAskResponse("messageResponse", selectedOption)
|
||||
@@ -1345,12 +1086,40 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// 10. Handle Ctrl+ shortcuts (Ctrl+A, Ctrl+E, Ctrl+W, etc.)
|
||||
if (key.ctrl && input && handleCtrlShortcut(input)) {
|
||||
return
|
||||
// Handle Ctrl+ shortcuts
|
||||
const keydown = input?.toLowerCase()
|
||||
if (key.ctrl && cursorPos && keydown) {
|
||||
switch (keydown) {
|
||||
case "u": // Ctrl+U, clear line before cursor
|
||||
if (cursorPos > 0) {
|
||||
setTextInput((prev) => prev.slice(cursorPos))
|
||||
setCursorPos(0)
|
||||
}
|
||||
return
|
||||
case "e": // Ctrl+E, move cursor to end
|
||||
setCursorPos(textInput.length)
|
||||
return
|
||||
case "b": // Ctrl+B, move cursor left
|
||||
setCursorPos((pos) => Math.max(0, pos - 1))
|
||||
return
|
||||
case "f": // Ctrl+F, move cursor right
|
||||
setCursorPos((pos) => Math.min(textInput.length, pos + 1))
|
||||
return
|
||||
case "d": // Ctrl+D, delete character after cursor
|
||||
if (cursorPos < textInput.length) {
|
||||
setTextInput((prev) => prev.slice(0, cursorPos) + prev.slice(cursorPos + 1))
|
||||
}
|
||||
return
|
||||
case "h": // Ctrl+H, delete character before cursor (like backspace)
|
||||
if (cursorPos > 0) {
|
||||
setTextInput((prev) => prev.slice(0, cursorPos - 1) + prev.slice(cursorPos))
|
||||
setCursorPos((pos) => pos - 1)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 11. Detect paste by checking if input length exceeds threshold
|
||||
// Detect paste by checking if input length exceeds threshold
|
||||
// Large pastes mess up the terminal UI, so we collapse them into a placeholder
|
||||
// Terminal sends large pastes in multiple chunks, so we combine chunks that arrive rapidly
|
||||
if (input && input.length > PASTE_COLLAPSE_THRESHOLD) {
|
||||
@@ -1378,10 +1147,10 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}
|
||||
pasteUpdateTimeoutRef.current = setTimeout(() => {
|
||||
const newPlaceholder = `[Pasted text #${pasteNum} +${activePasteLinesRef.current} lines]`
|
||||
const pattern = new RegExp(`\\[Pasted text #${pasteNum} \\+\\d+ lines\\]`)
|
||||
const newText = textInputRef.current.replace(pattern, newPlaceholder)
|
||||
textInputRef.current = newText // Update ref immediately so setCursorPos bounds check works
|
||||
setTextInput(newText)
|
||||
setTextInput((prev) => {
|
||||
const pattern = new RegExp(`\\[Pasted text #${pasteNum} \\+\\d+ lines\\]`)
|
||||
return prev.replace(pattern, newPlaceholder)
|
||||
})
|
||||
// Update cursor to be right after the placeholder
|
||||
setCursorPos(activePasteStartPosRef.current + newPlaceholder.length)
|
||||
Logger.info(`Paste #${pasteNum} complete: ${activePasteLinesRef.current} lines`)
|
||||
@@ -1394,8 +1163,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
pasteCounterRef.current += 1
|
||||
const pasteNum = pasteCounterRef.current
|
||||
activePasteNumRef.current = pasteNum
|
||||
const currentCursorPos = cursorPosRef.current // Use ref to avoid stale closure
|
||||
activePasteStartPosRef.current = currentCursorPos // Track where placeholder starts
|
||||
activePasteStartPosRef.current = cursorPos // Track where placeholder starts
|
||||
// Count line breaks in the pasted content (handle both \n and \r)
|
||||
const extraLines = input.match(/[\r\n]/g)?.length || 0
|
||||
activePasteLinesRef.current = extraLines // Track total lines
|
||||
@@ -1407,31 +1175,31 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
return next
|
||||
})
|
||||
|
||||
const newText =
|
||||
textInputRef.current.slice(0, currentCursorPos) + placeholder + textInputRef.current.slice(currentCursorPos)
|
||||
textInputRef.current = newText // Update ref immediately so setCursorPos bounds check works
|
||||
setTextInput(newText)
|
||||
setCursorPos(currentCursorPos + placeholder.length)
|
||||
setTextInput((prev) => prev.slice(0, cursorPos) + placeholder + prev.slice(cursorPos))
|
||||
setCursorPos(cursorPos + placeholder.length)
|
||||
return // Exit early - don't also add the raw input via normal handling below
|
||||
}
|
||||
|
||||
// 12. Normal input handling
|
||||
// Normal input handling
|
||||
if (key.shift && key.tab) {
|
||||
toggleAutoApproveAll()
|
||||
toggleYolo()
|
||||
return
|
||||
}
|
||||
if (key.tab && !mentionInfo.inMentionMode && !slashInfo.inSlashMode) {
|
||||
toggleMode()
|
||||
return
|
||||
}
|
||||
if (key.return && !mentionInfo.inMentionMode && !slashInfo.inSlashMode && !pendingAsk && !isSpinnerActive) {
|
||||
if (key.return && !mentionInfo.inMentionMode && !slashInfo.inSlashMode && !pendingAsk) {
|
||||
if (prompt.trim() || imagePaths.length > 0) {
|
||||
handleSubmit(prompt.trim(), imagePaths)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (key.backspace || key.delete) {
|
||||
deleteCharBefore()
|
||||
if (cursorPos > 0) {
|
||||
setTextInput((prev) => prev.slice(0, cursorPos - 1) + prev.slice(cursorPos))
|
||||
setCursorPos((pos) => pos - 1)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Cursor movement (when not in a menu)
|
||||
@@ -1440,41 +1208,26 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
return
|
||||
}
|
||||
if (key.rightArrow && !inSlashMenu && !inFileMenu) {
|
||||
setCursorPos((pos) => Math.min(textInputRef.current.length, pos + 1))
|
||||
setCursorPos((pos) => Math.min(textInput.length, pos + 1))
|
||||
return
|
||||
}
|
||||
if (key.upArrow && !inSlashMenu && !inFileMenu) {
|
||||
setCursorPos(moveCursorUp(textInputRef.current, cursorPosRef.current))
|
||||
setCursorPos(moveCursorUp(textInput, cursorPos))
|
||||
return
|
||||
}
|
||||
if (key.downArrow && !inSlashMenu && !inFileMenu) {
|
||||
setCursorPos(moveCursorDown(textInputRef.current, cursorPosRef.current))
|
||||
setCursorPos(moveCursorDown(textInput, cursorPos))
|
||||
return
|
||||
}
|
||||
// Normal input (single char or short paste)
|
||||
if (input && !key.ctrl && !key.meta && !key.upArrow && !key.downArrow && !key.tab) {
|
||||
insertTextAtCursor(input)
|
||||
setTextInput((prev) => prev.slice(0, cursorPos) + input + prev.slice(cursorPos))
|
||||
setCursorPos((pos) => pos + input.length)
|
||||
}
|
||||
})
|
||||
|
||||
const borderColor = mode === "act" ? COLORS.primaryBlue : "yellow"
|
||||
const metrics = getApiMetrics(messages)
|
||||
|
||||
// Get last API request total tokens for context window progress
|
||||
const lastApiReqTotalTokens = useMemo(() => getLastApiReqTotalTokens(messages), [messages])
|
||||
|
||||
// Get context window size from model info
|
||||
const contextWindowSize = useMemo(() => {
|
||||
const providerData = providerModels[provider]
|
||||
if (providerData && modelId in providerData.models) {
|
||||
const modelInfo = providerData.models[modelId] as ModelInfo
|
||||
if (modelInfo?.contextWindow) {
|
||||
return modelInfo.contextWindow
|
||||
}
|
||||
}
|
||||
return DEFAULT_CONTEXT_WINDOW
|
||||
}, [provider, modelId])
|
||||
|
||||
const showSlashMenu = slashInfo.inSlashMode && !slashMenuDismissed
|
||||
const showFileMenu = mentionInfo.inMentionMode && !showSlashMenu
|
||||
|
||||
@@ -1492,12 +1245,13 @@ 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" marginBottom={1}>
|
||||
<Box flexDirection="column" key="header">
|
||||
<StaticRobotFrame />
|
||||
<Text> </Text>
|
||||
<Text bold color="white">
|
||||
{centerText("What can I do for you?")}
|
||||
</Text>
|
||||
<Text> </Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1513,10 +1267,10 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
|
||||
{/* Dynamic region - only current streaming message + input */}
|
||||
<Box flexDirection="column" width="100%">
|
||||
{/* Animated robot and welcome text - only shown before messages start and user hasn't interacted */}
|
||||
{/* Animated robot and welcome text - only shown before messages start and user hasn't scrolled */}
|
||||
{isWelcomeState && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<AsciiMotionCli onInteraction={() => setUserScrolled(true)} />
|
||||
<AsciiMotionCli onScroll={() => setUserScrolled(true)} robotTopRow={robotTopRow} />
|
||||
<Text> </Text>
|
||||
<Text bold color="white">
|
||||
{centerText("What can I do for you?")}
|
||||
@@ -1567,7 +1321,6 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
<SettingsPanelContent
|
||||
controller={ctrl}
|
||||
initialMode={activePanel.initialMode}
|
||||
initialModelKey={activePanel.initialModelKey}
|
||||
onClose={() => setActivePanel(null)}
|
||||
/>
|
||||
)}
|
||||
@@ -1581,22 +1334,6 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Help panel */}
|
||||
{activePanel?.type === "help" && <HelpPanelContent onClose={() => setActivePanel(null)} />}
|
||||
|
||||
{/* Skills panel */}
|
||||
{activePanel?.type === "skills" && ctrl && (
|
||||
<SkillsPanelContent
|
||||
controller={ctrl}
|
||||
onClose={() => setActivePanel(null)}
|
||||
onUseSkill={(skillPath) => {
|
||||
setActivePanel(null)
|
||||
setTextInput(`@${skillPath} `)
|
||||
setCursorPos(skillPath.length + 2)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Slash command menu - below input (takes priority over file menu) */}
|
||||
{showSlashMenu && !activePanel && (
|
||||
<Box paddingLeft={1} paddingRight={1}>
|
||||
@@ -1636,7 +1373,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
{/* Row 1: Instructions (left, can wrap) | Plan/Act toggle (right, no wrap) */}
|
||||
<Box justifyContent="space-between" paddingLeft={1} paddingRight={1} width="100%">
|
||||
<Box flexShrink={1} flexWrap="wrap">
|
||||
<Text color="gray">/ for commands · @ for files</Text>
|
||||
<Text color="gray">@ for files · / for commands</Text>
|
||||
</Box>
|
||||
<Box flexShrink={0} gap={1}>
|
||||
<Box>
|
||||
@@ -1657,7 +1394,10 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
<Box paddingLeft={1} paddingRight={1}>
|
||||
<Text>
|
||||
{modelId} {(() => {
|
||||
const bar = createContextBar(lastApiReqTotalTokens, contextWindowSize)
|
||||
const bar = createContextBar(
|
||||
metrics.totalTokensIn + metrics.totalTokensOut,
|
||||
DEFAULT_CONTEXT_WINDOW,
|
||||
)
|
||||
return (
|
||||
<Text>
|
||||
<Text>{bar.filled}</Text>
|
||||
@@ -1665,7 +1405,8 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
</Text>
|
||||
)
|
||||
})()} <Text color="gray">
|
||||
({lastApiReqTotalTokens.toLocaleString()}) | ${metrics.totalCost.toFixed(3)}
|
||||
({(metrics.totalTokensIn + metrics.totalTokensOut).toLocaleString()}) | $
|
||||
{metrics.totalCost.toFixed(3)}
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -1688,17 +1429,17 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
|
||||
{/* Row 4: Auto-approve toggle */}
|
||||
<Box paddingLeft={1} paddingRight={1}>
|
||||
{autoApproveAll ? (
|
||||
<Text>
|
||||
<Text color="green">⏵⏵ Auto-approve all enabled</Text>
|
||||
<Text color="gray"> (Shift+Tab)</Text>
|
||||
</Text>
|
||||
{yolo ? (
|
||||
<Text color="green">⏵⏵ Auto-approve all enabled (Shift+Tab)</Text>
|
||||
) : (
|
||||
<Text color="gray">Auto-approve all disabled (Shift+Tab)</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Session summary - shown when exiting */}
|
||||
{isExiting && <SessionSummary />}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user