Compare commits

...

22 Commits

Author SHA1 Message Date
root ceebb8a9f3 Fix an issue where the message would refresh upon switching between plan/act 2025-05-05 10:39:00 -04:00
root d28a9b960c Remove unnecessary files 2025-05-02 22:17:40 -04:00
root badbd50f76 Consolidate UI refresh and welcome screen changesets 2025-05-02 22:17:19 -04:00
root 32cffa90a3 Enhance HomeHeader with full set of time-based greetings and messages 2025-05-02 22:07:27 -04:00
Elephant Lumps 7abfcafb53 new welcome header 2025-05-02 21:54:11 -04:00
root 2e62b03073 Fix: Formatting improvements in ChatView component 2025-05-02 20:32:53 -04:00
root bcf063d497 Add dynamic time-based greetings to welcome screen 2025-05-02 20:32:53 -04:00
root a1ea9799ab UI Refresh: Modernize Chat Interface with Personalized Welcome Screen 2025-05-02 10:48:33 -04:00
root 7d5fce3e37 UI Refresh: Modernize Chat Interface with Personalized Welcome Screen 2025-05-02 10:42:50 -04:00
nomaven 03d44105cc Adding copy button to code blocks (#3011)
* feat: add copy button to code blocks

* Adding ability to copy Code blocks

* feat: add OpenRouter base URL and balance display component

---------

Co-authored-by: ShlomoCode <78599753+ShlomoCode@users.noreply.github.com>
2025-05-01 00:51:28 -07:00
clicube 79b76fd783 feat: Support AWS Bedrock Application Inference Profiles for Cost Tracking (#2078)
* feat: Add support for custom model ID in AWS Bedrock provider

* preserve settings when switching Act-Plan modes

* Use base model ID for ApiHandler behavior determination when using a custom model on AWS Bedrock.
2025-05-01 12:17:28 +05:30
Evan 19cc8bc9f8 Add terminal connection timeout (#3218)
* add terminal connection timeout

* changeset

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-04-30 22:40:58 -05:00
Dennis Bartlett 08c04a3c67 Disable Codespell (#3224) 2025-04-30 19:21:09 -07:00
canvrno 41ae7326c0 Fix: Git mentions w/ no commits in workspace repo (#3179)
* fix for git mentions with no commits in repo
2025-04-30 19:15:35 -07:00
Trevor Hudson b0961f4538 Trevhud/remove linear (#3174)
* remove linear pull request action

* changeset
2025-04-30 18:51:16 -07:00
Tomás Barreiro 26242f6378 Fix tests: compile esmodules to cjs and bundle them (#3030)
* Run pretest in CI to build all tests

* Alias paths when running tests

* Bundle ES modules with esbuild

* alias packages
2025-04-30 17:51:32 -07:00
Tomás Barreiro 13228ed46f Fail the test workflow on test failures (#3197)
* Run pretest in CI to build all tests

* Alias paths when running tests

* Bundle ES modules with esbuild

* alias packages

* Preserve the test scripts exit code and display the output

* Remove outdated test
2025-04-30 17:51:03 -07:00
Tomás Barreiro d162a4b420 Alias paths on integration tests (#3196)
* Run pretest in CI to build all tests

* Alias paths when running tests
2025-04-30 17:49:40 -07:00
dependabot[bot] 1704684af8 Bump vite from 6.2.6 to 6.3.4 in /webview-ui in the npm_and_yarn group (#3214)
Bumps the npm_and_yarn group in /webview-ui with 1 update: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.2.6 to 6.3.4
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.4/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.4
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-30 17:25:43 -07:00
pashpashpash c63d9a13a5 Fix text to say "drop" instead of "drag" (#3221)
* drag -> drop

* text fix

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-04-30 17:18:48 -07:00
Frostbourne 65243adb24 [ENG-514] Introduce UI library (#3222)
* update tailwind

* install heroui

* Introduce HeroUIProvider and reorganize providers

* changeset

* use tailwind config

* use custom theme

* changeset

* Delete .changeset/large-boxes-behave.md
2025-04-30 17:18:18 -07:00
Tomás Barreiro e35f7b4e21 Run pretest in CI to build all tests (#2930) 2025-04-30 17:17:29 -07:00
45 changed files with 5013 additions and 432 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
updated drag and drop text to say "drop" instead of "drag"
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add support for custom model ID in AWS Bedrock provider, enabling use of Application Inference Profile.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Remove linear pull request action
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
UI Refresh: Enhanced welcome screen with dynamic time-based greetings that change according to the user's local time ("Good Morning", "Good Afternoon", or "Good Evening"). Added contextual random sub-messages that differ between normal working hours and late night sessions. This UI refresh transforms Cline's interface from a technical, feature-focused experience to a personal, welcoming environment that greets users by name. Visual improvements including rounded corners and distinct color-coding for Plan/Act modes create a more modern aesthetic while improving usability through better visual distinction between different states. By shifting from a capabilities-focused introduction to a personalized greeting with the Cline logo, the experience feels more human and approachable, potentially increasing user engagement and reducing the intimidation factor for those new to AI coding assistants.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix for git commit mentions in repos with no git commits
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add a timeout setting for the terminal connection, allowing users to adjust this if they are having timeout issues
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Added copy button to code blocks.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Introduce UI library for future UI development
+1
View File
@@ -13,6 +13,7 @@ permissions:
jobs:
codespell:
if: false
name: Check for spelling errors
runs-on: ubuntu-latest
-177
View File
@@ -1,177 +0,0 @@
name: Create Linear Issue on Pull Request
on:
workflow_dispatch:
pull_request:
branches: [main]
types: [opened]
permissions:
pull-requests: write
jobs:
create-linear-issue-on-pull-request:
runs-on: ubuntu-latest
steps:
- name: Check for existing Linear link
id: check-linear
uses: actions/github-script@v6
with:
result-encoding: string
script: |
const pr = context.payload.pull_request;
// 1) PR body
if (/https?:\/\/linear\.app/.test(pr.body||"")) {
return "true";
}
// 2) Any linked GitHub issues?
const res = await github.graphql(
`query($owner:String!,$repo:String!,$prNumber:Int!){
repository(owner:$owner,name:$repo){
pullRequest(number:$prNumber){
closingIssuesReferences(first:10){
nodes{number}
}
}
}
}`,
{
owner: context.repo.owner,
repo: context.repo.repo,
prNumber: pr.number
}
);
for (const {number} of res.repository.pullRequest.closingIssuesReferences.nodes) {
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: number
});
if (comments.data.some(c=>/https?:\/\/linear\.app/.test(c.body))) {
return "true";
}
}
return "false";
- name: Find or create Linear issue via GraphQL
if: steps.check-linear.outputs.result == 'false'
id: linear
uses: actions/github-script@v6
env:
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
with:
result-encoding: string
script: |
const API = 'https://api.linear.app/graphql';
const apiKey = process.env.LINEAR_API_KEY;
// Check if API key exists
if (!apiKey) {
core.setFailed('LINEAR_API_KEY is not set. Please add it to your repository secrets.');
core.setOutput('error', 'true');
core.setOutput('error-message', 'LINEAR_API_KEY is not set. Please add it to your repository secrets.');
return;
}
// Helper to call Linear with error handling
async function gql(q, v) {
try {
const r = await fetch(API, {
method:'POST',
headers:{
'Content-Type':'application/json',
'Authorization': apiKey
},
body: JSON.stringify({ query: q, variables: v })
});
if (!r.ok) {
throw new Error(`Linear API responded with status ${r.status}: ${await r.text()}`);
}
const json = await r.json();
// Check for GraphQL errors
if (json.errors && json.errors.length > 0) {
const errorMessages = json.errors.map(e => e.message).join(', ');
throw new Error(`Linear GraphQL errors: ${errorMessages}`);
}
return json.data;
} catch (error) {
core.error(`Error calling Linear API: ${error.message}`);
throw error;
}
}
try {
// 1) Set team ID
const teamId = "19b9c1b2-5f58-498c-b1bf-23ee8f52a677"
// 2) Look for existing issue by PR URL
const pr = context.payload.pull_request;
const searchData = await gql(
`query($team:ID!,$q:String!){
issues(filter: { team: { id: { eq: $team } } attachments: { some: { url: { eq: $q } } } }){nodes{id,url}}
}`,
{ team: teamId, q: pr.html_url }
);
let issue = searchData.issues.nodes[0];
// 3) Create if missing
if (!issue) {
const createData = await gql(
`mutation($input:IssueCreateInput!){
issueCreate(input:$input){issue{id,url}}
}`,
{
input: {
teamId,
title: `[GITHUB] ${pr.title}`,
description: `${pr.body||''}\n\n${pr.html_url}`,
stateId: "4d9bcba2-6712-47e3-b577-6ec1ee023dc2",
labelIds: ["504e7d60-5037-483f-a9b8-7e298bdf116f"]
}
}
);
issue = createData.issueCreate.issue;
}
// Set output for next steps
core.setOutput('linear-issue-url', issue.url);
core.setOutput('error', 'false');
} catch (error) {
core.setOutput('error', 'true');
core.setOutput('error-message', error.message);
core.setFailed(`Failed to create or find Linear issue: ${error.message}`);
}
- name: Comment PR with Linear link
if: steps.check-linear.outputs.result == 'false'
uses: actions/github-script@v6
with:
script: |
const pr = context.payload.pull_request;
const url = `${{ steps.linear.outputs.linear-issue-url }}`;
const body = `🔗 Linear issue created: ${url}`;
// Fetch existing comments
const { data: comments } = await github.rest.issues.listComments({
...context.repo,
issue_number: pr.number
});
const botComment = comments.find(c =>
c.user.type === "Bot" && c.body.startsWith("🔗 Linear issue created:")
);
if (botComment) {
await github.rest.issues.updateComment({
...context.repo,
comment_id: botComment.id,
body
});
} else {
await github.rest.issues.createComment({
...context.repo,
issue_number: pr.number,
body
});
}
+15 -3
View File
@@ -70,8 +70,8 @@ jobs:
run: npm run format
# Build the extension before running tests
- name: Build Extension
run: npm run compile
- name: Build Tests and Extension
run: npm run pretest
- name: Unit Tests
run: npm run test:unit
@@ -81,7 +81,7 @@ jobs:
id: extension_coverage
continue-on-error: true
run: |
xvfb-run -a npm run test:coverage > extension_coverage.txt 2>&1 || true
xvfb-run -a npm run test:coverage > extension_coverage.txt 2>&1
PYTHONPATH=.github/scripts python -m coverage_check extract-coverage extension_coverage.txt --type=extension --github-output --verbose
# Run webview tests with coverage
@@ -106,6 +106,18 @@ jobs:
webview-ui/webview_coverage.txt
retention-period: workflow # Artifacts are automatically deleted when the workflow completes
# Set the check as failed if any of the tests failed
- name: Check for test failures
run: |
# Check if any of the test steps failed
# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#steps-context
if [ "${{ steps.extension_coverage.outcome }}" != "success" ] || [ "${{ steps.webview_coverage.outcome }}" != "success" ]; then
echo "Tests failed."
cat extension_coverage.txt
cat webview-ui/webview_coverage.txt
exit 1
fi
coverage:
needs: test
runs-on: ubuntu-latest
+4
View File
@@ -6,6 +6,10 @@ export default defineConfig({
mocha: {
ui: "bdd",
timeout: 20000, // Maximum time (in ms) that a test can run before failing
/** Set up alias path resolution during tests
* @See {@link file://./test-setup.js}
*/
require: ["./test-setup.js"],
},
workspaceFolder: "test-workspace",
version: "stable",
+3
View File
@@ -0,0 +1,3 @@
<svg width="115" height="121" viewBox="0 0 115 121" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M114.317 68.0068L107.099 53.5136V45.1668C107.099 31.3312 95.9942 20.1262 82.2992 20.1262H69.9623C70.8552 18.2924 71.3456 16.231 71.3456 14.0557C71.3456 6.36649 65.1583 0.144287 57.5123 0.144287C49.8662 0.144287 43.6789 6.36649 43.6789 14.0557C43.6789 16.231 44.1693 18.2924 45.0622 20.1262H32.7254C19.0303 20.1262 7.9259 31.3312 7.9259 45.1668V53.5136L0.556479 67.9689C-0.185493 69.4232 -0.185493 71.1558 0.556479 72.6102L7.9259 86.9011V95.2479C7.9259 109.083 19.0303 120.289 32.7254 120.289H82.2992C95.9942 120.289 107.099 109.083 107.099 95.2479V86.9011L114.305 72.5596C115.021 71.1306 115.021 69.4485 114.317 68.0068ZM49.9668 79.8189C49.9668 86.1043 44.8987 91.201 38.6486 91.201C32.3984 91.201 27.3303 86.1043 27.3303 79.8189V59.5841C27.3303 53.2986 32.3984 48.202 38.6486 48.202C44.8987 48.202 49.9668 53.2986 49.9668 59.5841V79.8189ZM86.4366 79.8189C86.4366 86.1043 81.3685 91.201 75.1184 91.201C68.8682 91.201 63.8002 86.1043 63.8002 79.8189V59.5841C63.8002 53.2986 68.8682 48.202 75.1184 48.202C81.3685 48.202 86.4366 53.2986 86.4366 59.5841V79.8189Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+1
View File
@@ -19,6 +19,7 @@ const aliasResolverPlugin = {
"@services": path.resolve(__dirname, "src/services"),
"@shared": path.resolve(__dirname, "src/shared"),
"@utils": path.resolve(__dirname, "src/utils"),
"@packages": path.resolve(__dirname, "src/packages"),
}
// For each alias entry, create a resolver
+1 -1
View File
@@ -292,7 +292,7 @@
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
"protos": "node proto/build-proto.js && prettier src/shared/proto --write && prettier src/core/controller --write",
"compile-tests": "tsc -p ./tsconfig.test.json --outDir out",
"compile-tests": "node ./scripts/build-tests.js",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
"check-types": "tsc --noEmit",
+61
View File
@@ -0,0 +1,61 @@
const { execSync } = require("child_process")
const esbuild = require("esbuild")
const watch = process.argv.includes("--watch")
/**
* @type {import('esbuild').Plugin}
*/
const esbuildProblemMatcherPlugin = {
name: "esbuild-problem-matcher",
setup(build) {
build.onStart(() => {
console.log("[watch] build started")
})
build.onEnd((result) => {
result.errors.forEach(({ text, location }) => {
console.error(`✘ [ERROR] ${text}`)
console.error(` ${location.file}:${location.line}:${location.column}:`)
})
console.log("[watch] build finished")
})
},
}
const srcConfig = {
bundle: true,
minify: false,
sourcemap: true,
sourcesContent: true,
logLevel: "silent",
entryPoints: ["src/packages/**/*.ts"],
outdir: "out/packages",
format: "cjs",
platform: "node",
define: {
"process.env.IS_DEV": "true",
"process.env.IS_TEST": "true",
},
external: ["vscode"],
plugins: [esbuildProblemMatcherPlugin],
}
async function main() {
const srcCtx = await esbuild.context(srcConfig)
if (watch) {
await srcCtx.watch()
} else {
await srcCtx.rebuild()
await srcCtx.dispose()
}
}
execSync("tsc -p ./tsconfig.test.json --outDir out", { encoding: "utf-8" })
main().catch((e) => {
console.error(e)
process.exit(1)
})
+23 -6
View File
@@ -28,20 +28,26 @@ export class AwsBedrockHandler implements ApiHandler {
const modelId = await this.getModelId()
const model = this.getModel()
// This baseModelId is used to indicate the capabilities of the model.
// If the user selects a custom model, baseModelId will be set to the base model ID of the custom model.
// Otherwise, baseModelId will be the same as modelId.
const baseModelId =
(this.options.awsBedrockCustomSelected ? this.options.awsBedrockCustomModelBaseId : modelId) || modelId
// Check if this is an Amazon Nova model
if (modelId.includes("amazon.nova")) {
if (baseModelId.includes("amazon.nova")) {
yield* this.createNovaMessage(systemPrompt, messages, modelId, model)
return
}
// Check if this is a Deepseek model
if (modelId.includes("deepseek")) {
if (baseModelId.includes("deepseek")) {
yield* this.createDeepseekMessage(systemPrompt, messages, modelId, model)
return
}
const budget_tokens = this.options.thinkingBudgetTokens || 0
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
const reasoningOn = baseModelId.includes("3-7") && budget_tokens !== 0 ? true : false
// Get model info and message indices for caching
const userMsgIndices = messages.reduce((acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc), [] as number[])
@@ -167,12 +173,23 @@ export class AwsBedrockHandler implements ApiHandler {
}
}
getModel(): { id: BedrockModelId; info: ModelInfo } {
getModel(): { id: string; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in bedrockModels) {
const id = modelId as BedrockModelId
return { id, info: bedrockModels[id] }
}
const customSelected = this.options.awsBedrockCustomSelected
const baseModel = this.options.awsBedrockCustomModelBaseId
if (customSelected && modelId && baseModel && baseModel in bedrockModels) {
// Use the user-input model ID but inherit capabilities from the base model
return {
id: modelId,
info: bedrockModels[baseModel],
}
}
return {
id: bedrockDefaultModelId,
info: bedrockModels[bedrockDefaultModelId],
@@ -290,7 +307,7 @@ export class AwsBedrockHandler implements ApiHandler {
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
modelId: string,
model: { id: BedrockModelId; info: ModelInfo },
model: { id: string; info: ModelInfo },
): ApiStream {
// Get Bedrock client with proper credentials
const client = await this.getBedrockClient()
@@ -482,7 +499,7 @@ export class AwsBedrockHandler implements ApiHandler {
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
modelId: string,
model: { id: BedrockModelId; info: ModelInfo },
model: { id: string; info: ModelInfo },
): ApiStream {
// Get Bedrock client with proper credentials
const client = await this.getBedrockClient()
@@ -76,26 +76,6 @@ describe("ModelContextTracker", () => {
}
})
it("should throw an error when controller is dereferenced", async () => {
// Create a new tracker with a controller that will be garbage collected
const weakTracker = new ModelContextTracker(mockContext, taskId)
// Force the WeakRef to return null by overriding the deref method
const weakRef = { deref: sandbox.stub().returns(null) }
sandbox.stub(WeakRef.prototype, "deref").callsFake(() => weakRef.deref())
try {
// Try to call the method - this should throw
await weakTracker.recordModelUsage("any-provider", "any-model", "any-mode")
// If we get here, the test should fail
expect.fail("Expected an error to be thrown")
} catch (error) {
// Verify the error message
expect(error.message).to.equal("Unable to access extension context")
}
})
it("should append model usage to existing entries", async () => {
// Add an existing model usage entry
const existingTimestamp = 1617200000000
+46 -4
View File
@@ -136,8 +136,14 @@ export class Controller {
async initTask(task?: string, images?: string[], historyItem?: HistoryItem) {
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
const { apiConfiguration, customInstructions, autoApprovalSettings, browserSettings, chatSettings } =
await getAllExtensionState(this.context)
const {
apiConfiguration,
customInstructions,
autoApprovalSettings,
browserSettings,
chatSettings,
shellIntegrationTimeout,
} = await getAllExtensionState(this.context)
if (autoApprovalSettings) {
const updatedAutoApprovalSettings = {
@@ -159,6 +165,7 @@ export class Controller {
autoApprovalSettings,
browserSettings,
chatSettings,
shellIntegrationTimeout,
customInstructions,
task,
images,
@@ -784,6 +791,21 @@ export class Controller {
}
break
}
case "updateTerminalConnectionTimeout": {
if (message.shellIntegrationTimeout !== undefined) {
const timeout = message.shellIntegrationTimeout
if (typeof timeout === "number" && !isNaN(timeout) && timeout > 0) {
await updateGlobalState(this.context, "shellIntegrationTimeout", timeout)
await this.postStateToWebview()
} else {
console.warn(
`Invalid shell integration timeout value received: ${timeout}. ` + `Expected a positive number.`,
)
}
}
break
}
// Add more switch case statements here as more webview message commands
// are created within the webview context (i.e. inside media/main.js)
}
@@ -810,6 +832,8 @@ export class Controller {
previousModeVsCodeLmModelSelector: newVsCodeLmModelSelector,
previousModeThinkingBudgetTokens: newThinkingBudgetTokens,
previousModeReasoningEffort: newReasoningEffort,
previousModeAwsBedrockCustomSelected: newAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId: newAwsBedrockCustomModelBaseId,
planActSeparateModelsSetting,
} = await getAllExtensionState(this.context)
@@ -822,7 +846,6 @@ export class Controller {
await updateGlobalState(this.context, "previousModeReasoningEffort", apiConfiguration.reasoningEffort)
switch (apiConfiguration.apiProvider) {
case "anthropic":
case "bedrock":
case "vertex":
case "gemini":
case "asksage":
@@ -832,6 +855,19 @@ export class Controller {
case "xai":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
break
case "bedrock":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
await updateGlobalState(
this.context,
"previousModeAwsBedrockCustomSelected",
apiConfiguration.awsBedrockCustomSelected,
)
await updateGlobalState(
this.context,
"previousModeAwsBedrockCustomModelBaseId",
apiConfiguration.awsBedrockCustomModelBaseId,
)
break
case "openrouter":
case "cline":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.openRouterModelId)
@@ -877,7 +913,6 @@ export class Controller {
await updateGlobalState(this.context, "reasoningEffort", newReasoningEffort)
switch (newApiProvider) {
case "anthropic":
case "bedrock":
case "vertex":
case "gemini":
case "asksage":
@@ -887,6 +922,11 @@ export class Controller {
case "xai":
await updateGlobalState(this.context, "apiModelId", newModelId)
break
case "bedrock":
await updateGlobalState(this.context, "apiModelId", newModelId)
await updateGlobalState(this.context, "awsBedrockCustomSelected", newAwsBedrockCustomSelected)
await updateGlobalState(this.context, "awsBedrockCustomModelBaseId", newAwsBedrockCustomModelBaseId)
break
case "openrouter":
case "cline":
await updateGlobalState(this.context, "openRouterModelId", newModelId)
@@ -1769,6 +1809,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
telemetrySetting,
planActSeparateModelsSetting,
globalClineRulesToggles,
shellIntegrationTimeout,
} = await getAllExtensionState(this.context)
const localClineRulesToggles =
@@ -1798,6 +1839,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
vscMachineId: vscode.env.machineId,
globalClineRulesToggles: globalClineRulesToggles || {},
localClineRulesToggles: localClineRulesToggles || {},
shellIntegrationTimeout,
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ import { fileExistsAtPath } from "@utils/fs"
import { ClineMessage } from "@shared/ExtensionMessage"
import { TaskMetadata } from "@core/context/context-tracking/ContextTrackerTypes"
import os from "os"
import { execa } from "execa"
import { execa } from "@packages/execa"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
+5
View File
@@ -29,6 +29,8 @@ export type GlobalStateKey =
| "awsBedrockEndpoint"
| "awsProfile"
| "awsUseProfile"
| "awsBedrockCustomSelected"
| "awsBedrockCustomModelBaseId"
| "vertexProjectId"
| "vertexRegion"
| "lastShownAnnouncementId"
@@ -60,6 +62,8 @@ export type GlobalStateKey =
| "previousModeThinkingBudgetTokens"
| "previousModeReasoningEffort"
| "previousModeVsCodeLmModelSelector"
| "previousModeAwsBedrockCustomSelected"
| "previousModeAwsBedrockCustomModelBaseId"
| "previousModeModelInfo"
| "liteLlmBaseUrl"
| "liteLlmModelId"
@@ -76,5 +80,6 @@ export type GlobalStateKey =
| "planActSeparateModelsSetting"
| "favoritedModelIds"
| "requestTimeoutMs"
| "shellIntegrationTimeout"
export type LocalStateKey = "localClineRulesToggles"
+20 -1
View File
@@ -3,7 +3,7 @@ import { DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import { GlobalStateKey, SecretKey } from "./state-keys"
import { ApiConfiguration, ApiProvider, ModelInfo } from "@shared/api"
import { ApiConfiguration, ApiProvider, BedrockModelId, ModelInfo } from "@shared/api"
import { HistoryItem } from "@shared/HistoryItem"
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { BrowserSettings } from "@shared/BrowserSettings"
@@ -67,6 +67,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
awsBedrockEndpoint,
awsProfile,
awsUseProfile,
awsBedrockCustomSelected,
awsBedrockCustomModelBaseId,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
@@ -113,6 +115,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
previousModeVsCodeLmModelSelector,
previousModeThinkingBudgetTokens,
previousModeReasoningEffort,
previousModeAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId,
qwenApiLine,
liteLlmApiKey,
telemetrySetting,
@@ -126,6 +130,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
favoritedModelIds,
globalClineRulesToggles,
requestTimeoutMs,
shellIntegrationTimeout,
] = await Promise.all([
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
@@ -141,6 +146,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "awsBedrockEndpoint") as Promise<string | undefined>,
getGlobalState(context, "awsProfile") as Promise<string | undefined>,
getGlobalState(context, "awsUseProfile") as Promise<boolean | undefined>,
getGlobalState(context, "awsBedrockCustomSelected") as Promise<boolean | undefined>,
getGlobalState(context, "awsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
getGlobalState(context, "vertexProjectId") as Promise<string | undefined>,
getGlobalState(context, "vertexRegion") as Promise<string | undefined>,
getGlobalState(context, "openAiBaseUrl") as Promise<string | undefined>,
@@ -187,6 +194,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "previousModeVsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "previousModeThinkingBudgetTokens") as Promise<number | undefined>,
getGlobalState(context, "previousModeReasoningEffort") as Promise<string | undefined>,
getGlobalState(context, "previousModeAwsBedrockCustomSelected") as Promise<boolean | undefined>,
getGlobalState(context, "previousModeAwsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
getGlobalState(context, "qwenApiLine") as Promise<string | undefined>,
getSecret(context, "liteLlmApiKey") as Promise<string | undefined>,
getGlobalState(context, "telemetrySetting") as Promise<TelemetrySetting | undefined>,
@@ -200,6 +209,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "favoritedModelIds") as Promise<string[] | undefined>,
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
getGlobalState(context, "requestTimeoutMs") as Promise<number | undefined>,
getGlobalState(context, "shellIntegrationTimeout") as Promise<number | undefined>,
])
let apiProvider: ApiProvider
@@ -256,6 +266,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
awsBedrockEndpoint,
awsProfile,
awsUseProfile,
awsBedrockCustomSelected,
awsBedrockCustomModelBaseId,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
@@ -316,9 +328,12 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
previousModeVsCodeLmModelSelector,
previousModeThinkingBudgetTokens,
previousModeReasoningEffort,
previousModeAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId,
mcpMarketplaceEnabled,
telemetrySetting: telemetrySetting || "unset",
planActSeparateModelsSetting,
shellIntegrationTimeout: shellIntegrationTimeout || 4000,
}
}
@@ -337,6 +352,8 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
awsBedrockEndpoint,
awsProfile,
awsUseProfile,
awsBedrockCustomSelected,
awsBedrockCustomModelBaseId,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
@@ -394,6 +411,8 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
await updateGlobalState(context, "awsBedrockEndpoint", awsBedrockEndpoint)
await updateGlobalState(context, "awsProfile", awsProfile)
await updateGlobalState(context, "awsUseProfile", awsUseProfile)
await updateGlobalState(context, "awsBedrockCustomSelected", awsBedrockCustomSelected)
await updateGlobalState(context, "awsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
await updateGlobalState(context, "vertexProjectId", vertexProjectId)
await updateGlobalState(context, "vertexRegion", vertexRegion)
await updateGlobalState(context, "openAiBaseUrl", openAiBaseUrl)
+2
View File
@@ -173,6 +173,7 @@ export class Task {
autoApprovalSettings: AutoApprovalSettings,
browserSettings: BrowserSettings,
chatSettings: ChatSettings,
shellIntegrationTimeout: number,
customInstructions?: string,
task?: string,
images?: string[],
@@ -191,6 +192,7 @@ export class Task {
console.error("Failed to initialize ClineIgnoreController:", error)
})
this.terminalManager = new TerminalManager()
this.terminalManager.setShellIntegrationTimeout(shellIntegrationTimeout)
this.urlContentFetcher = new UrlContentFetcher(context)
this.browserSession = new BrowserSession(context, browserSettings)
this.contextManager = new ContextManager()
+29 -7
View File
@@ -39,7 +39,7 @@ const terminalManager = new TerminalManager(context);
const process = terminalManager.runCommand('npm install', '/path/to/project');
process.on('line', (line) => {
console.log(line);
console.log(line);
});
// To wait for the process to complete naturally:
@@ -93,6 +93,7 @@ export class TerminalManager {
private terminalIds: Set<number> = new Set()
private processes: Map<number, TerminalProcess> = new Map()
private disposables: vscode.Disposable[] = []
private shellIntegrationTimeout: number = 4000
constructor() {
let disposable: vscode.Disposable | undefined
@@ -144,13 +145,30 @@ export class TerminalManager {
process.run(terminalInfo.terminal, command)
} else {
// docs recommend waiting 3s for shell integration to activate
pWaitFor(() => terminalInfo.terminal.shellIntegration !== undefined, { timeout: 4000 }).finally(() => {
const existingProcess = this.processes.get(terminalInfo.id)
if (existingProcess && existingProcess.waitForShellIntegration) {
existingProcess.waitForShellIntegration = false
existingProcess.run(terminalInfo.terminal, command)
}
console.log(
`[TerminalManager Test] Waiting for shell integration for terminal ${terminalInfo.id} with timeout ${this.shellIntegrationTimeout}ms`,
)
pWaitFor(() => terminalInfo.terminal.shellIntegration !== undefined, {
timeout: this.shellIntegrationTimeout,
})
.then(() => {
console.log(
`[TerminalManager Test] Shell integration activated for terminal ${terminalInfo.id} within timeout.`,
)
})
.catch((err) => {
console.warn(
`[TerminalManager Test] Shell integration timed out or failed for terminal ${terminalInfo.id}: ${err.message}`,
)
})
.finally(() => {
console.log(`[TerminalManager Test] Proceeding with command execution for terminal ${terminalInfo.id}.`)
const existingProcess = this.processes.get(terminalInfo.id)
if (existingProcess && existingProcess.waitForShellIntegration) {
existingProcess.waitForShellIntegration = false
existingProcess.run(terminalInfo.terminal, command)
}
})
}
return mergePromise(process, promise)
@@ -219,4 +237,8 @@ export class TerminalManager {
this.disposables.forEach((disposable) => disposable.dispose())
this.disposables = []
}
setShellIntegrationTimeout(timeout: number): void {
this.shellIntegrationTimeout = timeout
}
}
+1
View File
@@ -0,0 +1 @@
export { execa } from "execa"
+1
View File
@@ -132,6 +132,7 @@ export interface ExtensionState {
shouldShowAnnouncement: boolean
taskHistory: HistoryItem[]
telemetrySetting: TelemetrySetting
shellIntegrationTimeout: number
uriScheme?: string
userInfo?: {
displayName: string | null
+2
View File
@@ -71,6 +71,7 @@ export interface WebviewMessage {
| "toggleClineRule"
| "deleteClineRule"
| "copyToClipboard"
| "updateTerminalConnectionTimeout"
// | "relaunchChromeDebugMode"
text?: string
@@ -121,6 +122,7 @@ export interface WebviewMessage {
filename?: string
offset?: number
shellIntegrationTimeout?: number
}
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
+2
View File
@@ -47,6 +47,8 @@ export interface ApiHandlerOptions {
awsUseProfile?: boolean
awsProfile?: string
awsBedrockEndpoint?: string
awsBedrockCustomSelected?: boolean
awsBedrockCustomModelBaseId?: BedrockModelId
vertexProjectId?: string
vertexRegion?: string
openAiBaseUrl?: string
+30 -2
View File
@@ -30,6 +30,15 @@ async function checkGitInstalled(): Promise<boolean> {
}
}
async function checkGitRepoHasCommits(cwd: string): Promise<boolean> {
try {
await execAsync("git rev-parse HEAD", { cwd })
return true
} catch (error) {
return false
}
}
export async function searchCommits(query: string, cwd: string): Promise<GitCommit[]> {
try {
const isInstalled = await checkGitInstalled()
@@ -44,6 +53,12 @@ export async function searchCommits(query: string, cwd: string): Promise<GitComm
return []
}
// Check if repo has any commits
if (!(await checkGitRepoHasCommits(cwd))) {
// No commits yet in the repository
return []
}
// Search commits by hash or message, limiting to 10 results
const { stdout } = await execAsync(
`git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short ` + `--grep="${query}" --regexp-ignore-case`,
@@ -100,6 +115,11 @@ export async function getCommitInfo(hash: string, cwd: string): Promise<string>
return "Not a git repository"
}
// Check if repo has any commits
if (!(await checkGitRepoHasCommits(cwd))) {
return "Repository has no commits yet"
}
// Get commit info, stats, and diff separately
const { stdout: info } = await execAsync(`git show --format="%H%n%h%n%s%n%an%n%ad%n%b" --no-patch ${hash}`, {
cwd,
@@ -147,8 +167,16 @@ export async function getWorkingState(cwd: string): Promise<string> {
return "No changes in working directory"
}
// Get all changes (both staged and unstaged) compared to HEAD
const { stdout: diff } = await execAsync("git diff HEAD", { cwd })
// Check if repo has any commits before trying to diff against HEAD
let diff = ""
if (await checkGitRepoHasCommits(cwd)) {
// Only run git diff if there are commits
const { stdout: diffOutput } = await execAsync("git diff HEAD", { cwd })
diff = diffOutput
} else {
// No commits yet, use status output only
return `Working directory changes (new repository):\n\n${status}`
}
const output = `Working directory changes:\n\n${status}\n\n${diff}`.trim()
return truncateOutput(output)
} catch (error) {
+22
View File
@@ -0,0 +1,22 @@
const tsConfigPaths = require("tsconfig-paths")
const fs = require("fs")
const tsConfig = JSON.parse(fs.readFileSync("./tsconfig.json", "utf-8"))
/**
* The aliases point towards the `src` directory.
* However, `tsc` doesn't compile paths by itself
* (https://www.typescriptlang.org/docs/handbook/modules/reference.html#paths-does-not-affect-emit)
* So we need to use tsconfig-paths to resolve the aliases when running tests,
* but pointing to `out` instead.
*/
const outPaths = {}
Object.keys(tsConfig.compilerOptions.paths).forEach((key) => {
const value = tsConfig.compilerOptions.paths[key]
outPaths[key] = value.map((path) => path.replace("src", "out"))
})
tsConfigPaths.register({
baseUrl: ".",
paths: outPaths,
})
+2 -1
View File
@@ -27,7 +27,8 @@
"@integrations/*": ["src/integrations/*"],
"@services/*": ["src/services/*"],
"@shared/*": ["src/shared/*"],
"@utils/*": ["src/utils/*"]
"@utils/*": ["src/utils/*"],
"@packages/*": ["src/packages/*"]
}
},
"include": ["src/**/*", "scripts/**/*"],
+4295 -162
View File
File diff suppressed because it is too large Load Diff
+5 -3
View File
@@ -15,11 +15,13 @@
},
"dependencies": {
"@floating-ui/react": "^0.27.4",
"@heroui/react": "^2.8.0-beta.2",
"@vscode/webview-ui-toolkit": "^1.4.0",
"debounce": "^2.1.1",
"dompurify": "^3.2.4",
"fast-deep-equal": "^3.1.3",
"firebase": "^11.3.0",
"framer-motion": "^12.7.4",
"fuse.js": "^7.0.0",
"fzf": "^0.5.2",
"mermaid": "^11.4.1",
@@ -42,7 +44,7 @@
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@tailwindcss/vite": "^4.0.12",
"@tailwindcss/vite": "^4.1.4",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
@@ -60,10 +62,10 @@
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"jsdom": "^26.0.0",
"tailwindcss": "^4.0.12",
"tailwindcss": "^4.1.4",
"typescript": "^5.7.3",
"typescript-eslint": "^8.18.2",
"vite": "^6.2.6",
"vite": "^6.3.4",
"vitest": "^3.0.5"
}
}
+5 -7
View File
@@ -6,10 +6,10 @@ import HistoryView from "./components/history/HistoryView"
import SettingsView from "./components/settings/SettingsView"
import WelcomeView from "./components/welcome/WelcomeView"
import AccountView from "./components/account/AccountView"
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
import { FirebaseAuthProvider } from "./context/FirebaseAuthContext"
import { useExtensionState } from "./context/ExtensionStateContext"
import { vscode } from "./utils/vscode"
import McpView from "./components/mcp/configuration/McpConfigurationView"
import { Providers } from "./Providers"
const AppContent = () => {
const { didHydrateState, showWelcome, shouldShowAnnouncement, showMcp, mcpTab } = useExtensionState()
@@ -126,11 +126,9 @@ const AppContent = () => {
const App = () => {
return (
<ExtensionStateContextProvider>
<FirebaseAuthProvider>
<AppContent />
</FirebaseAuthProvider>
</ExtensionStateContextProvider>
<Providers>
<AppContent />
</Providers>
)
}
+15
View File
@@ -0,0 +1,15 @@
import type { ReactNode } from "react"
import { ExtensionStateContextProvider } from "./context/ExtensionStateContext"
import { FirebaseAuthProvider } from "./context/FirebaseAuthContext"
import { HeroUIProvider } from "@heroui/react"
export function Providers({ children }: { children: ReactNode }) {
return (
<ExtensionStateContextProvider>
<FirebaseAuthProvider>
<HeroUIProvider>{children}</HeroUIProvider>
</FirebaseAuthProvider>
</ExtensionStateContextProvider>
)
}
+11 -16
View File
@@ -58,7 +58,8 @@ interface GitCommit {
description: string
}
const PLAN_MODE_COLOR = "var(--vscode-inputValidation-warningBorder)"
const PLAN_MODE_COLOR = "#955CF1" // Purple color for plan mode
const ACT_MODE_COLOR = "var(--vscode-terminal-ansiGreen)"
const SwitchOption = styled.div<{ isActive: boolean }>`
padding: 2px 8px;
@@ -93,7 +94,7 @@ const Slider = styled.div<{ isAct: boolean; isPlan?: boolean }>`
position: absolute;
height: 100%;
width: 50%;
background-color: ${(props) => (props.isPlan ? PLAN_MODE_COLOR : "var(--vscode-focusBorder)")};
background-color: ${(props) => (props.isPlan ? PLAN_MODE_COLOR : props.isAct ? ACT_MODE_COLOR : "var(--vscode-focusBorder)")};
transition: transform 0.2s ease;
transform: translateX(${(props) => (props.isAct ? "100%" : "0%")});
`
@@ -1287,7 +1288,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
inset: "10px 15px",
backgroundColor: "rgba(var(--vscode-errorForeground-rgb), 0.1)",
border: "2px solid var(--vscode-errorForeground)",
borderRadius: 2,
borderRadius: 4,
display: "flex",
alignItems: "center",
justifyContent: "center",
@@ -1337,7 +1338,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
position: "absolute",
inset: "10px 15px",
border: "1px solid var(--vscode-input-border)",
borderRadius: 2,
borderRadius: 4,
pointerEvents: "none",
zIndex: 5,
}}
@@ -1360,7 +1361,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
fontFamily: "var(--vscode-font-family)",
fontSize: "var(--vscode-editor-font-size)",
lineHeight: "var(--vscode-editor-line-height)",
borderRadius: 2,
borderRadius: 4,
borderLeft: 0,
borderRight: 0,
borderTop: 0,
@@ -1400,14 +1401,14 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}}
placeholder={showUnsupportedFileError ? "" : placeholderText}
maxRows={10}
minRows={3}
autoFocus={true}
style={{
width: "100%",
boxSizing: "border-box",
backgroundColor: "transparent",
color: "var(--vscode-input-foreground)",
//border: "1px solid var(--vscode-input-border)",
borderRadius: 2,
borderRadius: 4,
fontFamily: "var(--vscode-font-family)",
fontSize: "var(--vscode-editor-font-size)",
lineHeight: "var(--vscode-editor-line-height)",
@@ -1415,17 +1416,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
overflowX: "hidden",
overflowY: "scroll",
scrollbarWidth: "none",
// Since we have maxRows, when text is long enough it starts to overflow the bottom padding, appearing behind the thumbnails. To fix this, we use a transparent border to push the text up instead. (https://stackoverflow.com/questions/42631947/maintaining-a-padding-inside-of-text-area/52538410#52538410)
// borderTop: "9px solid transparent",
borderLeft: 0,
borderRight: 0,
borderTop: 0,
borderBottom: `${thumbnailsHeight + 6}px solid transparent`,
borderColor: "transparent",
// borderRight: "54px solid transparent",
// borderLeft: "9px solid transparent", // NOTE: react-textarea-autosize doesn't calculate correct height when using borderLeft/borderRight so we need to use horizontal padding instead
// Instead of using boxShadow, we use a div with a border to better replicate the behavior when the textarea is focused
// boxShadow: "0px 0px 0px 1px var(--vscode-input-border)",
padding: "9px 28px 3px 9px",
cursor: textAreaDisabled ? "not-allowed" : undefined,
flex: 1,
@@ -1434,7 +1429,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
isDraggingOver && !showUnsupportedFileError // Only show drag outline if not showing error
? "2px dashed var(--vscode-focusBorder)"
: isTextAreaFocused
? `1px solid ${chatSettings.mode === "plan" ? PLAN_MODE_COLOR : "var(--vscode-focusBorder)"}`
? `1px solid ${chatSettings.mode === "plan" ? PLAN_MODE_COLOR : ACT_MODE_COLOR}`
: "none",
outlineOffset: isDraggingOver && !showUnsupportedFileError ? "1px" : "0px", // Add offset for drag-over outline
}}
@@ -1461,7 +1456,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
right: 23,
display: "flex",
alignItems: "flex-center",
height: textAreaBaseHeight || 31,
height: 31,
bottom: 9.5, // should be 10 but doesn't look good on mac
zIndex: 2,
}}>
@@ -1616,7 +1611,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
color: "var(--vscode-descriptionForeground)",
whiteSpace: "nowrap",
}}>
Hold Shift to Drag Files
Hold Shift to Drop Files
</span>
</div>
</div>
+3 -13
View File
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useDeepCompareEffect, useEvent, useMount } from "react-use"
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
import styled from "styled-components"
import HomeHeader from "@/components/welcome/HomeHeader"
import {
ClineApiReqInfo,
ClineAsk,
@@ -914,21 +915,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
}}>
{telemetrySetting === "unset" && <TelemetryBanner />}
<HomeHeader />
{showAnnouncement && <Announcement version={version} hideAnnouncement={hideAnnouncement} />}
<div style={{ padding: "0 20px", flexShrink: 0 }}>
<h2>What can I do for you?</h2>
<p>
Thanks to{" "}
<VSCodeLink href="https://www.anthropic.com/claude/sonnet" style={{ display: "inline" }}>
Claude 3.7 Sonnet's
</VSCodeLink>
agentic coding capabilities, I can handle complex software development tasks step-by-step. With tools
that let me create & edit files, explore complex projects, use a browser, and execute terminal
commands (after you grant permission), I can assist you in ways that go beyond code completion or tech
support. I can even use MCP to create new tools and extend my own capabilities.
</p>
</div>
{taskHistory.length > 0 && <HistoryPreview showHistoryView={showHistoryView} />}
</div>
)}
@@ -1,4 +1,4 @@
import React, { memo, useEffect } from "react"
import React, { memo, useEffect, useRef, useState } from "react"
import type { ComponentProps } from "react"
import { useRemark } from "react-remark"
import rehypeHighlight, { Options } from "rehype-highlight"
@@ -91,15 +91,34 @@ const remarkPreventBoldFilenames = () => {
}
}
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
const CopyButton = styled(VSCodeButton)`
position: absolute;
top: 5px;
right: 5px;
z-index: 1;
opacity: 0;
`
const CodeBlockContainer = styled.div`
position: relative;
&:hover ${CopyButton} {
opacity: 1;
}
`
const StyledMarkdown = styled.div`
pre {
background-color: ${CODE_BLOCK_BG_COLOR};
border-radius: 3px;
margin: 13x 0;
margin: 13px 0;
padding: 10px 10px;
max-width: calc(100vw - 20px);
overflow-x: auto;
overflow-y: hidden;
padding-right: 70px;
}
pre > code {
@@ -197,8 +216,42 @@ const StyledPre = styled.pre<{ theme: any }>`
.join("")}
`
const PreWithCopyButton = ({
children,
theme,
...preProps
}: { theme: Record<string, string> } & React.HTMLAttributes<HTMLPreElement>) => {
const preRef = useRef<HTMLPreElement>(null)
const [copied, setCopied] = useState(false)
const handleCopy = () => {
if (preRef.current) {
const codeElement = preRef.current.querySelector("code")
const textToCopy = codeElement ? codeElement.textContent : preRef.current.textContent
if (!textToCopy) return
navigator.clipboard.writeText(textToCopy).then(() => {
setCopied(true)
setTimeout(() => setCopied(false), 1500)
})
}
}
return (
<CodeBlockContainer>
<CopyButton appearance="icon" onClick={handleCopy} aria-label={copied ? "Copied" : "Copy"}>
<span className={`codicon codicon-${copied ? "check" : "copy"}`}></span>
</CopyButton>
<StyledPre {...preProps} theme={theme} ref={preRef}>
{children}
</StyledPre>
</CodeBlockContainer>
)
}
const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
const { theme } = useExtensionState()
const [reactContent, setMarkdown] = useRemark({
remarkPlugins: [
remarkPreventBoldFilenames,
@@ -223,7 +276,7 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
],
rehypeReactOptions: {
components: {
pre: ({ node, children, ...preProps }: any) => {
pre: ({ children, ...preProps }: React.HTMLAttributes<HTMLPreElement>) => {
if (Array.isArray(children) && children.length === 1 && React.isValidElement(children[0])) {
const child = children[0] as React.ReactElement<{ className?: string }>
if (child.props?.className?.includes("language-mermaid")) {
@@ -231,9 +284,9 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
}
}
return (
<StyledPre {...preProps} theme={theme}>
<PreWithCopyButton {...preProps} theme={theme || {}}>
{children}
</StyledPre>
</PreWithCopyButton>
)
},
code: (props: ComponentProps<"code">) => {
@@ -253,7 +306,7 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
}, [markdown, setMarkdown, theme])
return (
<div style={{}}>
<div>
<StyledMarkdown>{reactContent}</StyledMarkdown>
</div>
)
@@ -769,6 +769,101 @@ const ApiOptions = ({
</>
)}
</p>
<label htmlFor="bedrock-model-dropdown">
<span style={{ fontWeight: 500 }}>Model</span>
</label>
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 2} className="dropdown-container">
<VSCodeDropdown
id="bedrock-model-dropdown"
value={apiConfiguration?.awsBedrockCustomSelected ? "custom" : selectedModelId}
onChange={(e: any) => {
const isCustom = e.target.value === "custom"
setApiConfiguration({
...apiConfiguration,
apiModelId: isCustom ? "" : e.target.value,
awsBedrockCustomSelected: isCustom,
awsBedrockCustomModelBaseId: bedrockDefaultModelId,
})
}}
style={{ width: "100%" }}>
<VSCodeOption value="">Select a model...</VSCodeOption>
{Object.keys(bedrockModels).map((modelId) => (
<VSCodeOption
key={modelId}
value={modelId}
style={{
whiteSpace: "normal",
wordWrap: "break-word",
maxWidth: "100%",
}}>
{modelId}
</VSCodeOption>
))}
<VSCodeOption value="custom">Custom</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
{apiConfiguration?.awsBedrockCustomSelected && (
<div>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Select "Custom" when using the Application Inference Profile in Bedrock. Enter the Application
Inference Profile ID in the Model ID field. However, be sure to encode the / in the ARN as %2F.
<br />
Example: arn:aws:bedrock:us-west-2:&lt;AWS Account
ID&gt;:application-inference-profile%2Fxxxxxxxxxxxx
</p>
<label htmlFor="bedrock-model-input">
<span style={{ fontWeight: 500 }}>Model ID</span>
</label>
<VSCodeTextField
id="bedrock-model-input"
value={apiConfiguration?.apiModelId || ""}
style={{ width: "100%", marginTop: 3 }}
onInput={handleInputChange("apiModelId")}
placeholder="Enter custom model ID..."
/>
<label htmlFor="bedrock-base-model-dropdown">
<span style={{ fontWeight: 500 }}>Base Inference Model</span>
</label>
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 3} className="dropdown-container">
<VSCodeDropdown
id="bedrock-base-model-dropdown"
value={apiConfiguration?.awsBedrockCustomModelBaseId || bedrockDefaultModelId}
onChange={handleInputChange("awsBedrockCustomModelBaseId")}
style={{ width: "100%" }}>
<VSCodeOption value="">Select a model...</VSCodeOption>
{Object.keys(bedrockModels).map((modelId) => (
<VSCodeOption
key={modelId}
value={modelId}
style={{
whiteSpace: "normal",
wordWrap: "break-word",
maxWidth: "100%",
}}>
{modelId}
</VSCodeOption>
))}
</VSCodeDropdown>
</DropdownContainer>
</div>
)}
{(selectedModelId === "anthropic.claude-3-7-sonnet-20250219-v1:0" ||
(apiConfiguration?.awsBedrockCustomSelected &&
apiConfiguration?.awsBedrockCustomModelBaseId === "anthropic.claude-3-7-sonnet-20250219-v1:0")) && (
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
)}
<ModelInfoView
selectedModelId={selectedModelId}
modelInfo={selectedModelInfo}
isDescriptionExpanded={isDescriptionExpanded}
setIsDescriptionExpanded={setIsDescriptionExpanded}
isPopup={isPopup}
/>
</div>
)}
@@ -1702,6 +1797,7 @@ const ApiOptions = ({
selectedProvider !== "vscode-lm" &&
selectedProvider !== "litellm" &&
selectedProvider !== "requesty" &&
selectedProvider !== "bedrock" &&
showModelOptions && (
<>
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 2} className="dropdown-container">
@@ -1709,7 +1805,6 @@ const ApiOptions = ({
<span style={{ fontWeight: 500 }}>Model</span>
</label>
{selectedProvider === "anthropic" && createDropdown(anthropicModels)}
{selectedProvider === "bedrock" && createDropdown(bedrockModels)}
{selectedProvider === "vertex" && createDropdown(vertexModels)}
{selectedProvider === "gemini" && createDropdown(geminiModels)}
{selectedProvider === "openai-native" && createDropdown(openAiNativeModels)}
@@ -1726,7 +1821,6 @@ const ApiOptions = ({
</DropdownContainer>
{((selectedProvider === "anthropic" && selectedModelId === "claude-3-7-sonnet-20250219") ||
(selectedProvider === "bedrock" && selectedModelId === "anthropic.claude-3-7-sonnet-20250219-v1:0") ||
(selectedProvider === "vertex" && selectedModelId === "claude-3-7-sonnet@20250219")) && (
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
)}
@@ -2048,6 +2142,14 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
case "anthropic":
return getProviderData(anthropicModels, anthropicDefaultModelId)
case "bedrock":
if (apiConfiguration?.awsBedrockCustomSelected) {
const baseModelId = apiConfiguration.awsBedrockCustomModelBaseId
return {
selectedProvider: provider,
selectedModelId: modelId || bedrockDefaultModelId,
selectedModelInfo: (baseModelId && bedrockModels[baseModelId]) || bedrockModels[bedrockDefaultModelId],
}
}
return getProviderData(bedrockModels, bedrockDefaultModelId)
case "vertex":
return getProviderData(vertexModels, vertexDefaultModelId)
@@ -9,6 +9,7 @@ import { TabButton } from "../mcp/configuration/McpConfigurationView"
import { useEvent } from "react-use"
import { ExtensionMessage } from "@shared/ExtensionMessage"
import BrowserSettingsSection from "./BrowserSettingsSection"
import TerminalSettingsSection from "./TerminalSettingsSection"
const { IS_DEV } = process.env
@@ -240,6 +241,9 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
{/* Browser Settings Section */}
<BrowserSettingsSection />
{/* Terminal Settings Section */}
<TerminalSettingsSection />
<div className="mt-auto pr-2 flex justify-center">
<SettingsButton
onClick={() => vscode.postMessage({ type: "openExtensionSettings" })}
@@ -0,0 +1,76 @@
import React, { useState } from "react"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { vscode } from "@/utils/vscode"
export const TerminalSettingsSection: React.FC = () => {
const { shellIntegrationTimeout, setShellIntegrationTimeout } = useExtensionState()
const [inputValue, setInputValue] = useState((shellIntegrationTimeout / 1000).toString())
const [inputError, setInputError] = useState<string | null>(null)
const handleTimeoutChange = (event: Event) => {
const target = event.target as HTMLInputElement
const value = target.value
setInputValue(value)
const seconds = parseFloat(value)
if (isNaN(seconds) || seconds <= 0) {
setInputError("Please enter a positive number")
return
}
setInputError(null)
const timeout = Math.round(seconds * 1000) // Convert to milliseconds
// Update local state
setShellIntegrationTimeout(timeout)
// Send to extension
vscode.postMessage({
type: "updateTerminalConnectionTimeout",
shellIntegrationTimeout: timeout,
})
}
const handleInputBlur = () => {
// If there was an error, reset the input to the current valid value
if (inputError) {
setInputValue((shellIntegrationTimeout / 1000).toString())
setInputError(null)
}
}
return (
<div
id="terminal-settings-section"
style={{ marginBottom: 20, borderTop: "1px solid var(--vscode-panel-border)", paddingTop: 15 }}>
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 10px 0", fontSize: "14px" }}>Terminal Settings</h3>
<div style={{ marginBottom: 15 }}>
<div style={{ marginBottom: 8 }}>
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
Shell integration timeout (seconds)
</label>
<div style={{ display: "flex", alignItems: "center" }}>
<VSCodeTextField
style={{ width: "100%" }}
value={inputValue}
placeholder="Enter timeout in seconds"
onChange={(event) => handleTimeoutChange(event as Event)}
onBlur={handleInputBlur}
/>
</div>
{inputError && (
<div style={{ color: "var(--vscode-errorForeground)", fontSize: "12px", marginTop: 5 }}>{inputError}</div>
)}
</div>
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)", margin: 0 }}>
Set how long Cline waits for shell integration to activate before executing commands. Increase this value if
you experience terminal connection timeouts.
</p>
</div>
</div>
)
}
export default TerminalSettingsSection
@@ -0,0 +1,93 @@
import { useState, useEffect } from "react"
import { useFirebaseAuth } from "@/context/FirebaseAuthContext"
import ClineLogoWhite from "@/assets/ClineLogoWhite"
type TimeOfDay = "morning" | "afternoon" | "evening" | "night"
const getTimeOfDay = (): TimeOfDay => {
const hour = new Date().getHours()
if (hour >= 5 && hour < 12) return "morning"
if (hour >= 12 && hour < 18) return "afternoon"
if (hour >= 18 && hour < 24) return "evening"
return "night"
}
const secondaryMessages: Record<TimeOfDay, string[]> = {
morning: ["Grab coffee and let's get to work.", "Ready for a productive day?", "What are we building today?"],
afternoon: ["Let's keep the momentum going.", "Time for the next task."],
evening: ["Still going strong!", "Let's get those final touches in."],
night: [
"Ah, the silence of deep focus… or deep chaos.",
"Running on caffeine and pure determination?",
"The world sleeps. You debug.",
"Back again? The bugs didn't stand a chance.",
"Let's code like no one's watching — because no one is.",
"Quiet hours, loud ideas.",
"Running low on sleep, high on inspiration?",
"The best commits are made under moonlight.",
"If this isn't dedication, I don't know what is.",
"Brainstorming or bug-hunting — I'm with you either way.",
"Burning the midnight oil?",
],
}
// Should never hit default, just built in for redundancy
const defaultSecondaryMessages = ["Let's get to work.", "Ready when you are.", "How can I assist?"]
const primaryGreetings: Record<TimeOfDay, string[]> = {
morning: ["Good Morning", "Morning", "Top o' the mornin'"],
afternoon: ["Good Afternoon", "Afternoon", "Howdy"],
evening: ["Good Evening", "Evening"],
night: ["Good Evening", "Evening"],
}
// Should never hit default, just built in for redundancy
const defaultPrimaryGreetings = ["Hello", "Hi", "Hey!", "Yo!"]
const getSecondaryMessage = (timeOfDay: TimeOfDay): string => {
const messages = secondaryMessages[timeOfDay] || defaultSecondaryMessages
const randomIndex = Math.floor(Math.random() * messages.length)
return messages[randomIndex]
}
const getPrimaryGreeting = (timeOfDay: TimeOfDay): string => {
const greetings = primaryGreetings[timeOfDay] || defaultPrimaryGreetings
const randomIndex = Math.floor(Math.random() * greetings.length)
return greetings[randomIndex]
}
const getFirstName = (displayName: string | null | undefined) => {
if (!displayName) return ""
return displayName.split(" ")[0]
}
const HomeHeader = () => {
const { user } = useFirebaseAuth()
const [timeOfDay] = useState<TimeOfDay>(getTimeOfDay())
const [greeting] = useState<string>(getPrimaryGreeting(timeOfDay))
const [secondaryMessage] = useState<string>(getSecondaryMessage(timeOfDay))
const [firstName, setFirstName] = useState<string>("")
// Calculate firstName only once when the component mounts or when user changes
useEffect(() => {
setFirstName(getFirstName(user?.displayName))
}, [user?.displayName])
return (
<div className="flex flex-col items-center mb-5">
<div className="my-5">
<ClineLogoWhite className="size-16" />
</div>
<div className="text-center">
<h2 className="m-0 text-[var(--vscode-font-size)]">
{greeting}
{firstName ? `, ${firstName}` : ""}!
</h2>
<div className="text-[var(--vscode-descriptionForeground)] text-sm font-normal mt-1">{secondaryMessage}</div>
</div>
</div>
)
}
export default HomeHeader
@@ -39,6 +39,7 @@ interface ExtensionStateContextType extends ExtensionState {
setTelemetrySetting: (value: TelemetrySetting) => void
setShowAnnouncement: (value: boolean) => void
setPlanActSeparateModelsSetting: (value: boolean) => void
setShellIntegrationTimeout: (value: number) => void
setMcpServers: (value: McpServer[]) => void
// Navigation
@@ -69,6 +70,7 @@ export const ExtensionStateContextProvider: React.FC<{
planActSeparateModelsSetting: true,
globalClineRulesToggles: {},
localClineRulesToggles: {},
shellIntegrationTimeout: 4000, // default timeout for shell integration
})
const [didHydrateState, setDidHydrateState] = useState(false)
const [showWelcome, setShowWelcome] = useState(false)
@@ -242,6 +244,11 @@ export const ExtensionStateContextProvider: React.FC<{
...prevState,
shouldShowAnnouncement: value,
})),
setShellIntegrationTimeout: (value) =>
setState((prevState) => ({
...prevState,
shellIntegrationTimeout: value,
})),
setMcpServers: (mcpServers: McpServer[]) => setMcpServers(mcpServers),
setShowMcp,
setMcpTab,
+2
View File
@@ -4,6 +4,8 @@
/* @import "tailwindcss/preflight.css" layer(base); */
@import "tailwindcss/utilities.css" layer(utilities);
@config "../tailwind.config.js";
textarea:focus {
outline: 1.5px solid var(--vscode-focusBorder, #007fd4);
}
+22
View File
@@ -0,0 +1,22 @@
const { heroui } = require("@heroui/react")
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}", "./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
darkMode: "class",
plugins: [
heroui({
defaultTheme: "vscode",
themes: {
vscode: {
colors: {
background: "",
},
},
},
}),
],
}