Bladeren bron

Merge branch 'main' into dependabot/npm_and_yarn/mammoth-1.11.0

Catriel Müller 3 maanden geleden
bovenliggende
commit
7382d3d328
100 gewijzigde bestanden met toevoegingen van 5693 en 816 verwijderingen
  1. 5 0
      .changeset/afraid-points-fall.md
  2. 0 5
      .changeset/green-melons-retire.md
  3. 5 0
      .changeset/orange-wasps-agree.md
  4. 0 10
      .changeset/tender-files-leave.md
  5. 0 5
      .changeset/tidy-mugs-cheat.md
  6. 5 5
      .github/copilot-instructions.md
  7. 20 8
      .github/workflows/marketplace-publish.yml
  8. 47 0
      CHANGELOG.md
  9. 8 1
      apps/kilocode-docs/docs/cli.md
  10. 21 0
      apps/kilocode-docs/docs/features/auto-approving-actions.md
  11. 4 4
      apps/kilocode-docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/api-configuration-profiles.md
  12. 2 2
      apps/playwright-e2e/package.json
  13. 1 1
      apps/storybook/package.json
  14. 1 1
      apps/vscode-e2e/package.json
  15. 0 1
      apps/web-evals/package.json
  16. 70 0
      cli/CHANGELOG.md
  17. 5 0
      cli/README.md
  18. 2 1
      cli/esbuild.config.mjs
  19. 641 30
      cli/npm-shrinkwrap.dist.json
  20. 3 2
      cli/package.dist.json
  21. 5 4
      cli/package.json
  22. 21 0
      cli/scripts/copy-extension.mjs
  23. 84 0
      cli/src/cli.ts
  24. 339 0
      cli/src/commands/checkpoint.ts
  25. 3 1
      cli/src/commands/core/types.ts
  26. 2 0
      cli/src/commands/index.ts
  27. 0 2
      cli/src/commands/tasks.ts
  28. 253 0
      cli/src/config/__tests__/env-overrides.test.ts
  29. 339 0
      cli/src/config/__tests__/openConfig.test.ts
  30. 142 0
      cli/src/config/env-overrides.ts
  31. 30 4
      cli/src/config/openConfig.ts
  32. 64 0
      cli/src/config/schema.json
  33. 2 0
      cli/src/constants/keyboard/keyCodes.ts
  34. 1 0
      cli/src/constants/providers/__tests__/models.test.ts
  35. 1 0
      cli/src/constants/providers/labels.ts
  36. 10 0
      cli/src/constants/providers/models.ts
  37. 18 1
      cli/src/constants/providers/settings.ts
  38. 1 0
      cli/src/constants/providers/validation.ts
  39. 51 1
      cli/src/host/ExtensionHost.ts
  40. 30 6
      cli/src/host/VSCode.ts
  41. 296 0
      cli/src/host/__tests__/ExtensionHost.raceCondition.test.ts
  42. 177 0
      cli/src/host/__tests__/webview-async-resolution.test.ts
  43. 14 0
      cli/src/index.ts
  44. 16 1
      cli/src/services/__tests__/ExtensionService.test.ts
  45. 1 0
      cli/src/services/autocomplete.ts
  46. 9 2
      cli/src/services/extension.ts
  47. 673 0
      cli/src/state/atoms/__tests__/shell.test.ts
  48. 5 2
      cli/src/state/atoms/approval.ts
  49. 6 1
      cli/src/state/atoms/config.ts
  50. 12 0
      cli/src/state/atoms/extension.ts
  51. 73 2
      cli/src/state/atoms/keyboard.ts
  52. 214 0
      cli/src/state/atoms/shell.ts
  53. 9 1
      cli/src/state/atoms/ui.ts
  54. 61 25
      cli/src/state/hooks/useApprovalHandler.ts
  55. 4 1
      cli/src/state/hooks/useCommandContext.ts
  56. 14 1
      cli/src/state/hooks/useCommandInput.ts
  57. 14 1
      cli/src/state/hooks/useHotkeys.ts
  58. 5 0
      cli/src/types/messages.ts
  59. 9 0
      cli/src/ui/UI.tsx
  60. 76 41
      cli/src/ui/components/CommandInput.tsx
  61. 4 0
      cli/src/ui/messages/extension/AskMessageRouter.tsx
  62. 48 0
      cli/src/ui/messages/extension/ask/AskCheckpointRestoreMessage.tsx
  63. 5 9
      cli/src/ui/messages/extension/ask/AskUseMcpServerMessage.tsx
  64. 1 0
      cli/src/ui/messages/extension/ask/index.ts
  65. 3 6
      cli/src/ui/messages/extension/say/SayMcpServerResponseMessage.tsx
  66. 1 1
      cli/src/ui/messages/extension/utils.ts
  67. 40 1
      cli/src/ui/utils/keyParsing.ts
  68. 4 3
      cli/turbo.json
  69. 3 2
      jetbrains/host/package.json
  70. 254 221
      jetbrains/host/pnpm-lock.yaml
  71. 2 2
      package.json
  72. 1 2
      packages/evals/package.json
  73. 17 10
      packages/evals/src/cli/runUnitTest.ts
  74. 1 116
      packages/types/src/__tests__/kilocode.test.ts
  75. 1 0
      packages/types/src/global-settings.ts
  76. 0 84
      packages/types/src/kilocode/kilocode.ts
  77. 4 0
      packages/types/src/kilocode/native-function-calling.ts
  78. 1 0
      packages/types/src/message.ts
  79. 22 3
      packages/types/src/provider-settings.ts
  80. 13 0
      packages/types/src/providers/fireworks.ts
  81. 1 0
      packages/types/src/providers/index.ts
  82. 39 0
      packages/types/src/providers/minimax.ts
  83. 24 1
      packages/types/src/providers/moonshot.ts
  84. 11 0
      packages/types/src/providers/synthetic.ts
  85. 199 163
      pnpm-lock.yaml
  86. 26 4
      scripts/reset-kilocode-state.sh
  87. 5 2
      src/api/index.ts
  88. 94 0
      src/api/providers/__tests__/kilocode-openrouter.spec.ts
  89. 298 0
      src/api/providers/__tests__/minimax.spec.ts
  90. 1 1
      src/api/providers/__tests__/moonshot.spec.ts
  91. 90 2
      src/api/providers/anthropic.ts
  92. 2 1
      src/api/providers/index.ts
  93. 62 4
      src/api/providers/kilocode-openrouter.ts
  94. 39 0
      src/api/providers/kilocode/IFimProvider.ts
  95. 16 0
      src/api/providers/kilocode/nativeToolCallHelpers.ts
  96. 253 0
      src/api/providers/minimax-anthropic.ts
  97. 15 2
      src/api/providers/openrouter.ts
  98. 21 3
      src/api/providers/qwen-code.ts
  99. 130 0
      src/api/transform/kilocode/reasoning-details.ts
  100. 18 0
      src/api/transform/openai-format.ts

+ 5 - 0
.changeset/afraid-points-fall.md

@@ -0,0 +1,5 @@
+---
+"@kilocode/cli": patch
+---
+
+Fix initialization race conditions on auto mode

+ 0 - 5
.changeset/green-melons-retire.md

@@ -1,5 +0,0 @@
----
-"kilo-code": minor
----
-
-Add Native MCP Support for JSON Tool Calling

+ 5 - 0
.changeset/orange-wasps-agree.md

@@ -0,0 +1,5 @@
+---
+"kilo-code": patch
+---
+
+Added GLM 4.6 to Fireworks provider

+ 0 - 10
.changeset/tender-files-leave.md

@@ -1,10 +0,0 @@
----
-"kilo-code": patch
----
-
-fix(virtual-quota): display active model in UI for the frontend
-
-When the backend switches the model, it now sends out a "model has changed" signal by emitting event.
-The main application logic catches this signal and immediately tells the user interface to refresh itself.
-The user interface then updates the display to show the name of the new, currently active model.
-This will also keep the backend and the frontend active model in sync

+ 0 - 5
.changeset/tidy-mugs-cheat.md

@@ -1,5 +0,0 @@
----
-"@kilocode/cli": patch
----
-
-File mention suggestion - @my/file

+ 5 - 5
.github/copilot-instructions.md

@@ -51,11 +51,6 @@ let j = 3
 
 ## Special Cases
 
-### Kilocode specific file
-
-- if the filename or directory name contains kilocode no marking with comments is required
-- if the file lives inside of the jetbrains/ or cli/ root folder, no marking with comments is required
-
 ### New Files
 
 If you're creating a completely new file that doesn't exist in Roo, add this comment at the top:
@@ -63,3 +58,8 @@ If you're creating a completely new file that doesn't exist in Roo, add this com
 ```
 // kilocode_change - new file
 ```
+
+### Kilocode specific file - these rules take precedence over all other rules above
+
+- if the filename or directory name contains kilocode no marking with comments is required
+- if the file lives inside of the jetbrains/ or cli/ root folder, no marking with comments is required

+ 20 - 8
.github/workflows/marketplace-publish.yml

@@ -1,5 +1,7 @@
 name: Publish Extension
 on:
+    pull_request:
+        types: [closed]
     workflow_dispatch:
 
 env:
@@ -14,6 +16,11 @@ jobs:
         runs-on: ubuntu-latest
         permissions:
             contents: write # Required for pushing tags.
+        if: >
+            ( github.event_name == 'pull_request' &&
+            github.event.pull_request.base.ref == 'main' &&
+            contains(github.event.pull_request.title, 'Changeset version bump') ) ||
+            github.event_name == 'workflow_dispatch'
         steps:
             - name: Checkout code
               uses: actions/checkout@v4
@@ -103,6 +110,11 @@ jobs:
     publish-jetbrains:
         needs: publish-extension
         runs-on: ubuntu-latest
+        if: >
+            ( github.event_name == 'pull_request' &&
+            github.event.pull_request.base.ref == 'main' &&
+            contains(github.event.pull_request.title, 'Changeset version bump') ) ||
+            github.event_name == 'workflow_dispatch'
         steps:
             - name: Checkout code
               uses: actions/checkout@v4
@@ -165,11 +177,11 @@ jobs:
                   path: jetbrains/plugin/build/distributions/${{ env.BUNDLE_NAME }}
             - name: JetBrains Marketplace Publisher
               run: |
-                  curl \
-                  -X POST \
-                  -H "Authorization: Bearer ${{ secrets.JETBRAINS_MARKETPLACE_TOKEN }}" \
-                  -F "file=@jetbrains/plugin/build/distributions/${{ env.BUNDLE_NAME }}" \
-                  -F "pluginId=28350" \
-                  -F "channel=stable" \
-                  -F "isHidden=false" \
-                  https://plugins.jetbrains.com/plugin/uploadPlugin
+                 curl \
+                 -X POST \
+                 -H "Authorization: Bearer ${{ secrets.JETBRAINS_MARKETPLACE_TOKEN }}" \
+                 -F "file=@jetbrains/plugin/build/distributions/${{ env.BUNDLE_NAME }}" \
+                 -F "pluginId=28350" \
+                 -F "channel=stable" \
+                 -F "isHidden=false" \
+                 https://plugins.jetbrains.com/plugin/uploadPlugin

+ 47 - 0
CHANGELOG.md

@@ -1,5 +1,52 @@
 # kilo-code
 
+## [v4.118.0]
+
+- [#3638](https://github.com/Kilo-Org/kilocode/pull/3638) [`49e44fc`](https://github.com/Kilo-Org/kilocode/commit/49e44fc1c3c02648a534f737c6df0d7d4964810c) Thanks [@mcowger](https://github.com/mcowger)! - Enable Moonshot for native tool calling
+
+- [#3295](https://github.com/Kilo-Org/kilocode/pull/3295) [`5a155a9`](https://github.com/Kilo-Org/kilocode/commit/5a155a9825e20f10bfc752baff37cd5de53980b2) Thanks [@Maosghoul](https://github.com/Maosghoul)! - MiniMax provider added. MiniMax provider preserves reasoning blocks and has experimental support for native tool calling.
+
+- [#3632](https://github.com/Kilo-Org/kilocode/pull/3632) [`d7fad58`](https://github.com/Kilo-Org/kilocode/commit/d7fad58673da95de682bf5d7f38a90a288daae03) Thanks [@iscekic](https://github.com/iscekic)! - Introduces "YOLO" mode, where all approval requests are automatically approved. Initially used for `--auto` mode in the CLI, now available in the extension as well in `Settings > Auto-Approval`.
+
+- [#3605](https://github.com/Kilo-Org/kilocode/pull/3605) [`03fccd3`](https://github.com/Kilo-Org/kilocode/commit/03fccd3a3c75186c320aad3754547bf1619cf424) Thanks [@viktorxhzj](https://github.com/viktorxhzj)! - OpenRouter and Kilo Gateway providers now preserve reasoning blocks between API requests. This should improve performance of reasoning models, especially MiniMax M2.
+
+- [#3597](https://github.com/Kilo-Org/kilocode/pull/3597) [`ea3c0bd`](https://github.com/Kilo-Org/kilocode/commit/ea3c0bda8055f3ad3370c5794803ae176fefadd4) Thanks [@mcowger](https://github.com/mcowger)! - Add Kimi K2 Thinking to Moonshot.ai provider.
+
+### Patch Changes
+
+- [#3500](https://github.com/Kilo-Org/kilocode/pull/3500) [`2e1a536`](https://github.com/Kilo-Org/kilocode/commit/2e1a53678fc1c331d98a63f0ab15b02b53fc1625) Thanks [@iscekic](https://github.com/iscekic)! - improves windows support
+
+- [#3629](https://github.com/Kilo-Org/kilocode/pull/3629) [`fefc671`](https://github.com/Kilo-Org/kilocode/commit/fefc671535bbfb1036c7088219d45e45d00cbad1) Thanks [@chrarnoldus](https://github.com/chrarnoldus)! - Anthropic provider now preserves reasoning blocks and has (experimental) support for native (JSON-style) tool calls. This greatly improves support for Claude Haiku 4.5
+
+- [#3612](https://github.com/Kilo-Org/kilocode/pull/3612) [`970e799`](https://github.com/Kilo-Org/kilocode/commit/970e799473111922eee13d859fd29cb3f7abf715) Thanks [@burkostya](https://github.com/burkostya)! - fix(native-tools): Make read_file_multi pattern JSON Schema compliant
+
+## [v4.117.0]
+
+- [#3568](https://github.com/Kilo-Org/kilocode/pull/3568) [`18dfc86`](https://github.com/Kilo-Org/kilocode/commit/18dfc86e5f00e0d722f448450574ec444d3c894a) Thanks [@mcowger](https://github.com/mcowger)! - Add Kimi K2-Thinking to Synthetic Provider
+
+## [v4.116.1]
+
+- [#3533](https://github.com/Kilo-Org/kilocode/pull/3533) [`f5bb82d`](https://github.com/Kilo-Org/kilocode/commit/f5bb82ddf4038ed2d9e5a1266c9e6b0dc09c0af5) Thanks [@chrarnoldus](https://github.com/chrarnoldus)! - Fix hang at startup
+
+## [v4.116.0]
+
+- [#3288](https://github.com/Kilo-Org/kilocode/pull/3288) [`afeca17`](https://github.com/Kilo-Org/kilocode/commit/afeca176f4ef7d227831715b5e5a672fcf3fe58f) Thanks [@mcowger](https://github.com/mcowger)! - Add Native MCP Support for JSON Tool Calling
+
+### Patch Changes
+
+- [#3471](https://github.com/Kilo-Org/kilocode/pull/3471) [`9895a95`](https://github.com/Kilo-Org/kilocode/commit/9895a959b9bb8a14aab6ec11267a2bb0e12fb78c) Thanks [@chrarnoldus](https://github.com/chrarnoldus)! - Allow native tool calling fro Qwen Code provider
+
+- [#3513](https://github.com/Kilo-Org/kilocode/pull/3513) [`ff2e459`](https://github.com/Kilo-Org/kilocode/commit/ff2e4595777683265559f81f82dd9cbb0dc2e9f3) Thanks [@markijbema](https://github.com/markijbema)! - Prevent autocomplete from suggesting duplicating the previous or next line
+
+- [#3523](https://github.com/Kilo-Org/kilocode/pull/3523) [`ba5416a`](https://github.com/Kilo-Org/kilocode/commit/ba5416ae3083fb5225ed7e9f0e1018203e611b84) Thanks [@markijbema](https://github.com/markijbema)! - Removed the gutter animation for autocomplete
+
+- [#2893](https://github.com/Kilo-Org/kilocode/pull/2893) [`37d8493`](https://github.com/Kilo-Org/kilocode/commit/37d8493a4d2629d0498f089b40f850ddae0c91fc) Thanks [@ivanarifin](https://github.com/ivanarifin)! - fix(virtual-quota): display active model in UI for the frontend
+
+    When the backend switches the model, it now sends out a "model has changed" signal by emitting event.
+    The main application logic catches this signal and immediately tells the user interface to refresh itself.
+    The user interface then updates the display to show the name of the new, currently active model.
+    This will also keep the backend and the frontend active model in sync
+
 ## [v4.115.0]
 
 - [#3486](https://github.com/Kilo-Org/kilocode/pull/3486) [`2b89d84`](https://github.com/Kilo-Org/kilocode/commit/2b89d8472123e48db866e10a88b5b6160812d73e) Thanks [@markijbema](https://github.com/markijbema)! - Show MCP tool instead of server name when asked to approve a tool

+ 8 - 1
apps/kilocode-docs/docs/cli.md

@@ -244,6 +244,14 @@ This instructs the AI to proceed without user input.
       echo "Implement the new feature" | kilocode --auto --timeout 600
 ```
 
+## Environment Variable Overrides
+
+The CLI supports overriding config values with environment variables. The supported environment variables are:
+
+- `KILO_PROVIDER`: Override the active provider ID
+- For `kilocode` provider: `KILOCODE_<FIELD_NAME>` (e.g., `KILOCODE_MODEL` → `kilocodeModel`)
+- For other providers: `KILO_<FIELD_NAME>` (e.g., `KILO_API_KEY` → `apiKey`)
+
 ## Local Development
 
 ### DevTools
@@ -257,4 +265,3 @@ Use the `/teams` command to see a list of all organizations you can switch into.
 Use `/teams select` and start typing the team name to switch teams.
 
 The process is the same when switching into a Team or Enterprise organization.
-The process is the same when switching into a Team or Enterprise organization.

+ 21 - 0
apps/kilocode-docs/docs/features/auto-approving-actions.md

@@ -293,3 +293,24 @@ This setting allows Kilo Code to automatically update task progress and todo lis
 
 This is particularly useful when combined with the Subtasks permission, as it allows Kilo Code to maintain a complete picture of project progress without constant approval requests.
 :::
+
+## YOLO mode
+
+:::danger YOLO Mode (Risk: Maximum)
+
+**"You Only Live Once"** mode enables _all_ auto-approve permissions at once using the master toggle. This gives Kilo Code complete autonomy to read files, write code, execute commands, and perform any operation without asking for permission.
+
+**When to use:**
+
+- Rapid prototyping in isolated environments
+- Trusted, low-stakes projects
+- When you want maximum AI autonomy
+
+**When NOT to use:**
+
+- Production code or sensitive projects
+- Working with important data
+- Any situation where mistakes could be costly
+
+This is the fastest way to work with Kilo Code, but also the riskiest. Use it only when you fully trust the AI and are prepared for the consequences.
+:::

+ 4 - 4
apps/kilocode-docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/api-configuration-profiles.md

@@ -36,19 +36,19 @@ API 配置配置文件允许您创建和切换不同的 AI 设置集。每个配
 
     - 选择您的 API 提供商
 
-            <img src="/docs/img/api-configuration-profiles/api-configuration-profiles-2.png" alt="提供商选择下拉菜单" width="550" />
+              <img src="/docs/img/api-configuration-profiles/api-configuration-profiles-2.png" alt="提供商选择下拉菜单" width="550" />
 
     - 输入 API 密钥
 
-            <img src="/docs/img/api-configuration-profiles/api-configuration-profiles-3.png" alt="API 密钥输入字段" width="550" />
+              <img src="/docs/img/api-configuration-profiles/api-configuration-profiles-3.png" alt="API 密钥输入字段" width="550" />
 
     - 选择模型
 
-            <img src="/docs/img/api-configuration-profiles/api-configuration-profiles-8.png" alt="模型选择界面" width="550" />
+              <img src="/docs/img/api-configuration-profiles/api-configuration-profiles-8.png" alt="模型选择界面" width="550" />
 
     - 调整模型参数
 
-            <img src="/docs/img/api-configuration-profiles/api-configuration-profiles-5.png" alt="模型参数调整控件" width="550" />
+              <img src="/docs/img/api-configuration-profiles/api-configuration-profiles-5.png" alt="模型参数调整控件" width="550" />
 
 ### 切换配置文件
 

+ 2 - 2
apps/playwright-e2e/package.json

@@ -12,14 +12,14 @@
 		"clean": "rimraf test-results playwright-report .turbo"
 	},
 	"devDependencies": {
-		"@playwright/test": "^1.53.1",
+		"@playwright/test": "^1.56.1",
 		"@roo-code/config-eslint": "workspace:^",
 		"@roo-code/config-typescript": "workspace:^",
 		"@roo-code/types": "workspace:^",
 		"@types/node": "^20.x",
 		"@vscode/test-electron": "^2.4.0",
 		"dotenv": "^16.4.5",
-		"rimraf": "^6.0.1",
+		"rimraf": "^6.1.0",
 		"typescript": "5.8.3"
 	},
 	"dependencies": {

+ 1 - 1
apps/storybook/package.json

@@ -41,7 +41,7 @@
 		"chromatic": "^13.0.0",
 		"jsonc-parser": "^3.3.1",
 		"node-fetch": "2",
-		"rimraf": "^6.0.1",
+		"rimraf": "^6.1.0",
 		"storybook": "^9.0.18",
 		"tailwindcss-animate": "^1.0.7",
 		"typescript": "^5.4.5",

+ 1 - 1
apps/vscode-e2e/package.json

@@ -20,7 +20,7 @@
 		"@vscode/test-electron": "^2.4.0",
 		"glob": "^11.0.1",
 		"mocha": "^11.1.0",
-		"rimraf": "^6.0.1",
+		"rimraf": "^6.1.0",
 		"typescript": "5.8.3"
 	}
 }

+ 0 - 1
apps/web-evals/package.json

@@ -51,7 +51,6 @@
 		"@roo-code/config-eslint": "workspace:^",
 		"@roo-code/config-typescript": "workspace:^",
 		"@tailwindcss/postcss": "^4",
-		"@types/ps-tree": "^1.1.6",
 		"@types/react": "^18.3.23",
 		"@types/react-dom": "^18.3.5",
 		"tailwindcss": "^4",

+ 70 - 0
cli/CHANGELOG.md

@@ -1,5 +1,75 @@
 # @kilocode/cli
 
+## 0.3.0
+
+### Minor Changes
+
+- [#3623](https://github.com/Kilo-Org/kilocode/pull/3623) [`ef6bcac`](https://github.com/Kilo-Org/kilocode/commit/ef6bcac79ed5708996e80c0c943d52f12c0fe3b2) Thanks [@Sureshkumars](https://github.com/Sureshkumars)! - # Checkpoint Restore
+
+    Allows users to restore their conversation to a previous point in time.
+
+    ## What do we have here?
+
+    ### View your checkpoints
+
+    `/checkpoint list`
+
+    This shows all available restore points with:
+    Hash for the checkpoint
+    When it was created
+
+    ### Restore to a checkpoint
+
+    `/checkpoint restore abc123...`
+
+    You'll see a confirmation showing:
+    Which checkpoint you're going back to
+    How many messages will be removed
+    What will happen to your current work
+
+    Choose `Restore` to go back, or `Cancel` to keep working.
+
+    ### Example
+
+    Let's say you asked Kilo CLI to refactor some code, but you don't like the result:
+
+    Run `/checkpoint list` to see earlier save points
+
+    Find the checkpoint from before the refactoring
+
+    Run `/checkpoint restore <hash>` with that checkpoint's hash
+
+    Confirm the restore
+    Your conversation is now back to before the refactoring happened
+
+    ### Why use checkpoints?
+
+    1. Undo mistakes - Go back if something went wrong
+    2. Try different approaches - Restore and try a different solution
+    3. Keep working states - Return to a point where everything was working
+
+### Patch Changes
+
+- [#3500](https://github.com/Kilo-Org/kilocode/pull/3500) [`2e1a536`](https://github.com/Kilo-Org/kilocode/commit/2e1a53678fc1c331d98a63f0ab15b02b53fc1625) Thanks [@iscekic](https://github.com/iscekic)! - improves windows support
+
+- [#3641](https://github.com/Kilo-Org/kilocode/pull/3641) [`94bc43a`](https://github.com/Kilo-Org/kilocode/commit/94bc43af224fed36023d0f3571d39c04d21aa660) Thanks [@KrtinShet](https://github.com/KrtinShet)! - Fix workspace path resolution when using relative paths with --workspace flag. Bash commands now execute in the correct directory.
+
+## 0.2.0
+
+### Minor Changes
+
+- [#3528](https://github.com/Kilo-Org/kilocode/pull/3528) [`77438f1`](https://github.com/Kilo-Org/kilocode/commit/77438f1dfe2e9b5cfc5faccc314130d82c299842) Thanks [@KrtinShet](https://github.com/KrtinShet) [@iscekic](https://github.com/iscekic)! - add shell mode
+
+- [#3556](https://github.com/Kilo-Org/kilocode/pull/3556) [`0fd4e8f`](https://github.com/Kilo-Org/kilocode/commit/0fd4e8f3b130f86ae5932c33ab647a2a08742c55) Thanks [@iscekic](https://github.com/iscekic)! - adds support for overriding config with env vars
+
+## 0.1.2
+
+### Patch Changes
+
+- [#3259](https://github.com/Kilo-Org/kilocode/pull/3259) [`9e50bca`](https://github.com/Kilo-Org/kilocode/commit/9e50bcaebb93383eca1dac8e23ff02339c910ed9) Thanks [@stennkool](https://github.com/stennkool)! - Continue the last task conversation in the workspace (-c argument)
+
+- [#3491](https://github.com/Kilo-Org/kilocode/pull/3491) [`b884c9e`](https://github.com/Kilo-Org/kilocode/commit/b884c9ea220f3c4c3a9c147f0fece64a26c830b4) Thanks [@catrielmuller](https://github.com/catrielmuller)! - File mention suggestion - @my/file
+
 ## 0.1.1
 
 ### Patch Changes

+ 5 - 0
cli/README.md

@@ -43,6 +43,11 @@ kilocode --mode architect
 
 # Start with a specific workspace
 kilocode --workspace /path/to/project
+
+# Resume the last conversation from this workspace
+kilocode -c
+# or
+kilocode --continue
 ```
 
 ### Parallel mode

+ 2 - 1
cli/esbuild.config.mjs

@@ -136,7 +136,7 @@ const __dirname = __dirname__(__filename);
 		"pkce-challenge",
 		"pretty-bytes",
 		"proper-lockfile",
-		"ps-tree",
+		"ps-list",
 		"puppeteer-chromium-resolver",
 		"puppeteer-core",
 		"react",
@@ -149,6 +149,7 @@ const __dirname = __dirname__(__filename);
 		"simple-git",
 		"socket.io-client",
 		"sound-play",
+		"sqlite3",
 		"stream-json",
 		"strip-bom",
 		"tiktoken",

File diff suppressed because it is too large
+ 641 - 30
cli/npm-shrinkwrap.dist.json


+ 3 - 2
cli/package.dist.json

@@ -1,6 +1,6 @@
 {
 	"name": "@kilocode/cli",
-	"version": "0.1.1",
+	"version": "0.3.0",
 	"description": "Terminal User Interface for Kilo Code",
 	"type": "module",
 	"main": "index.js",
@@ -80,7 +80,7 @@
 		"posthog-node": "^4.2.1",
 		"pretty-bytes": "^7.0.0",
 		"proper-lockfile": "^4.1.2",
-		"ps-tree": "^1.2.0",
+		"ps-list": "^8.1.1",
 		"puppeteer-chromium-resolver": "^24.0.2",
 		"puppeteer-core": "^23.4.0",
 		"react": "^19.2.0",
@@ -95,6 +95,7 @@
 		"simple-git": "^3.27.0",
 		"socket.io-client": "^4.8.1",
 		"sound-play": "^1.1.0",
+		"sqlite3": "^5.1.7",
 		"stream-json": "^1.8.0",
 		"string-width": "^8.1.0",
 		"strip-ansi": "^7.1.0",

+ 5 - 4
cli/package.json

@@ -1,6 +1,6 @@
 {
 	"name": "@kilocode/cli",
-	"version": "0.1.1",
+	"version": "0.3.0",
 	"description": "Terminal User Interface for Kilo Code",
 	"type": "module",
 	"main": "dist/index.js",
@@ -23,7 +23,7 @@
 		"logs": "clear && tail -f ~/.kilocode/cli/logs/cli.txt",
 		"logs:clear": "rimraf ~/.kilocode/cli/logs/* && touch ~/.kilocode/cli/logs/cli.txt",
 		"clean:kilocode": "npx del-cli ./dist/kilocode --force && npx mkdirp ./dist/kilocode",
-		"copy:kilocode": "npx cpy '../bin-unpacked/extension/**' './dist/kilocode' --parents",
+		"copy:kilocode": "node scripts/copy-extension.mjs",
 		"lint": "eslint .",
 		"changeset:version": "jq --arg version \"$(jq -r '.version' package.json)\" '.version = $version' package.dist.json > tmp.json && mv tmp.json package.dist.json && prettier --write package.dist.json"
 	},
@@ -102,7 +102,7 @@
 		"posthog-node": "^4.2.1",
 		"pretty-bytes": "^7.0.0",
 		"proper-lockfile": "^4.1.2",
-		"ps-tree": "^1.2.0",
+		"ps-list": "^8.1.1",
 		"puppeteer-chromium-resolver": "^24.0.2",
 		"puppeteer-core": "^23.4.0",
 		"react": "^19.2.0",
@@ -117,6 +117,7 @@
 		"simple-git": "^3.27.0",
 		"socket.io-client": "^4.8.1",
 		"sound-play": "^1.1.0",
+		"sqlite3": "^5.1.7",
 		"stream-json": "^1.8.0",
 		"string-width": "^8.1.0",
 		"strip-ansi": "^7.1.0",
@@ -159,7 +160,7 @@
 		"ink-testing-library": "^4.0.0",
 		"mkdirp": "^3.0.1",
 		"prettier": "^3.4.2",
-		"rimraf": "^6.0.1",
+		"rimraf": "^6.1.0",
 		"tsx": "^4.19.3",
 		"typescript": "^5.4.5",
 		"vitest": "^3.2.3"

+ 21 - 0
cli/scripts/copy-extension.mjs

@@ -0,0 +1,21 @@
+import { cpSync } from "fs"
+import { fileURLToPath } from "url"
+import { dirname, join } from "path"
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = dirname(__filename)
+const cliDir = join(__dirname, "..")
+
+const source = join(cliDir, "..", "bin-unpacked", "extension")
+const dest = join(cliDir, "dist", "kilocode")
+
+console.log(`Copying from: ${source}`)
+console.log(`Copying to: ${dest}`)
+
+try {
+	cpSync(source, dest, { recursive: true })
+	console.log("✓ Extension files copied successfully")
+} catch (error) {
+	console.error("Error copying extension files:", error)
+	process.exit(1)
+}

+ 84 - 0
cli/src/cli.ts

@@ -11,6 +11,9 @@ import { loadConfigAtom, mappedExtensionStateAtom, providersAtom } from "./state
 import { ciExitReasonAtom } from "./state/atoms/ci.js"
 import { requestRouterModelsAtom } from "./state/atoms/actions.js"
 import { loadHistoryAtom } from "./state/atoms/history.js"
+import { taskHistoryDataAtom, updateTaskHistoryFiltersAtom } from "./state/atoms/taskHistory.js"
+import { sendWebviewMessageAtom } from "./state/atoms/actions.js"
+import { taskResumedViaContinueAtom } from "./state/atoms/extension.js"
 import { getTelemetryService, getIdentityManager } from "./services/telemetry/index.js"
 import { notificationsAtom, notificationsErrorAtom, notificationsLoadingAtom } from "./state/atoms/notifications.js"
 import { fetchKilocodeNotifications } from "./utils/notifications.js"
@@ -27,6 +30,7 @@ export interface CLIOptions {
 	timeout?: number
 	parallel?: boolean
 	worktreeBranch?: string | undefined
+	continue?: boolean
 }
 
 /**
@@ -140,6 +144,11 @@ export class CLI {
 				void this.fetchNotifications()
 			}
 
+			// Resume conversation if continue mode is enabled
+			if (this.options.continue) {
+				await this.resumeLastConversation()
+			}
+
 			this.isInitialized = true
 			logs.info("Kilo Code CLI initialized successfully", "CLI")
 		} catch (error) {
@@ -361,6 +370,81 @@ export class CLI {
 		}
 	}
 
+	/**
+	 * Resume the last conversation from the current workspace
+	 */
+	private async resumeLastConversation(): Promise<void> {
+		if (!this.service || !this.store) {
+			logs.error("Cannot resume conversation: service or store not available", "CLI")
+			throw new Error("Service or store not initialized")
+		}
+
+		const workspace = this.options.workspace || process.cwd()
+
+		try {
+			logs.info("Attempting to resume last conversation", "CLI", { workspace })
+
+			// Update filters to current workspace and newest sort
+			await this.store.set(updateTaskHistoryFiltersAtom, {
+				workspace: "current",
+				sort: "newest",
+				favoritesOnly: false,
+			})
+
+			// Send task history request to extension
+			await this.store.set(sendWebviewMessageAtom, {
+				type: "taskHistoryRequest",
+				payload: {
+					requestId: Date.now().toString(),
+					workspace: "current",
+					sort: "newest",
+					favoritesOnly: false,
+					pageIndex: 0,
+				},
+			})
+
+			// Wait for the data to arrive (the response will update taskHistoryDataAtom through effects)
+			await new Promise((resolve) => setTimeout(resolve, 2000))
+
+			// Get the task history data
+			const taskHistoryData = this.store.get(taskHistoryDataAtom)
+
+			if (!taskHistoryData || !taskHistoryData.historyItems || taskHistoryData.historyItems.length === 0) {
+				logs.warn("No previous tasks found for workspace", "CLI", { workspace })
+				console.error("\nNo previous tasks found for this workspace. Please start a new conversation.\n")
+				process.exit(1)
+				return // TypeScript doesn't know process.exit stops execution
+			}
+
+			// Find the most recent task (first in the list since we sorted by newest)
+			const lastTask = taskHistoryData.historyItems[0]
+
+			if (!lastTask) {
+				logs.warn("No valid task found in history", "CLI", { workspace })
+				console.error("\nNo valid task found to resume. Please start a new conversation.\n")
+				process.exit(1)
+				return
+			}
+
+			logs.debug("Found last task", "CLI", { taskId: lastTask.id, task: lastTask.task })
+
+			// Send message to resume the task
+			await this.store.set(sendWebviewMessageAtom, {
+				type: "showTaskWithId",
+				text: lastTask.id,
+			})
+
+			// Mark that the task was resumed via --continue to prevent showing "Task ready to resume" message
+			this.store.set(taskResumedViaContinueAtom, true)
+
+			logs.info("Task resume initiated", "CLI", { taskId: lastTask.id, task: lastTask.task })
+		} catch (error) {
+			logs.error("Failed to resume conversation", "CLI", { error, workspace })
+			console.error("\nFailed to resume conversation. Please try starting a new conversation.\n")
+			process.exit(1)
+		}
+	}
+
 	/**
 	 * Get the ExtensionService instance
 	 */

+ 339 - 0
cli/src/commands/checkpoint.ts

@@ -0,0 +1,339 @@
+/**
+ * /checkpoint command - Manage and revert to checkpoints
+ */
+
+import type { Command, CommandContext, ArgumentProviderContext, ArgumentSuggestion } from "./core/types.js"
+import { logs } from "../services/logs.js"
+import { ExtensionMessage } from "../types/messages.js"
+
+/**
+ * Interface for checkpoint message from chatMessages
+ */
+interface CheckpointMessage {
+	ts: number
+	type: "say"
+	say: string
+	text?: string
+	metadata?: {
+		type?: string
+		fromHash?: string
+		toHash?: string
+		suppressMessage?: boolean
+	}
+}
+
+/**
+ * Extract checkpoint messages from chatMessages
+ */
+function getCheckpointMessages(chatMessages: ExtensionMessage[]): CheckpointMessage[] {
+	return chatMessages
+		.filter((msg): msg is CheckpointMessage => msg.type === "say" && msg.say === "checkpoint_saved" && !!msg.text)
+		.reverse() // Most recent first
+}
+
+/**
+ * Find checkpoint by hash
+ */
+function findCheckpointByHash(
+	checkpoints: CheckpointMessage[],
+	hash: string,
+): { message: CheckpointMessage; hash: string } | null {
+	const lowerHash = hash.toLowerCase()
+
+	const exactMatch = checkpoints.find((cp) => cp.text?.toLowerCase() === lowerHash)
+	if (exactMatch && exactMatch.text) {
+		return { message: exactMatch, hash: exactMatch.text }
+	}
+
+	return null
+}
+
+/**
+ * Format timestamp to human-readable format
+ */
+function formatTimestamp(ts: number): string {
+	const date = new Date(ts)
+	const now = new Date()
+	const diffMs = now.getTime() - date.getTime()
+	const diffMins = Math.floor(diffMs / (1000 * 60))
+	const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
+
+	// Relative time for recent checkpoints
+	if (diffMins < 1) {
+		return "just now"
+	}
+	if (diffMins < 60) {
+		return `${diffMins} minute${diffMins === 1 ? "" : "s"} ago`
+	}
+	if (diffHours < 24) {
+		return `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`
+	}
+
+	// Absolute time for older checkpoints
+	const timeStr = date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit", hour12: true })
+	const dateStr = date.toLocaleDateString([], { month: "short", day: "numeric" })
+
+	return `${timeStr}, ${dateStr}`
+}
+
+/**
+ * Handle /checkpoint list
+ */
+async function handleList(context: CommandContext): Promise<void> {
+	const { chatMessages, addMessage } = context
+	const checkpoints = getCheckpointMessages(chatMessages)
+
+	logs.debug("Listing checkpoints", "checkpoint", { count: checkpoints.length })
+
+	if (checkpoints.length === 0) {
+		logs.info("No checkpoints found", "checkpoint")
+		addMessage({
+			id: Date.now().toString(),
+			type: "system",
+			content: "No checkpoints available.",
+			ts: Date.now(),
+		})
+		return
+	}
+
+	const lines = ["**Available checkpoints:**", ""]
+
+	checkpoints.forEach((cp) => {
+		const hash = cp.text || "unknown"
+		const timestamp = formatTimestamp(cp.ts)
+		const isSuppressed = cp.metadata?.suppressMessage === true
+		const suppressedLabel = isSuppressed ? " [auto-saved]" : ""
+
+		lines.push(`  ${hash} - ${timestamp}${suppressedLabel}`)
+	})
+
+	lines.push("")
+	lines.push("Use `/checkpoint restore <hash>` to revert.")
+
+	addMessage({
+		id: Date.now().toString(),
+		type: "system",
+		content: lines.join("\n"),
+		ts: Date.now(),
+	})
+}
+
+/**
+ * Handle /checkpoint restore
+ */
+async function handleRestore(context: CommandContext, hash: string): Promise<void> {
+	const { chatMessages, addMessage, sendMessage } = context
+	const checkpoints = getCheckpointMessages(chatMessages)
+
+	logs.debug("Finding checkpoint for restore", "checkpoint", { hash, checkpointCount: checkpoints.length })
+
+	const result = findCheckpointByHash(checkpoints, hash)
+
+	if (!result) {
+		logs.warn("Checkpoint not found for restore", "checkpoint", { hash })
+		addMessage({
+			id: Date.now().toString(),
+			type: "error",
+			content: `Checkpoint "${hash}" not found. Use /checkpoint list to see available checkpoints.`,
+			ts: Date.now(),
+		})
+		return
+	}
+
+	const { message, hash: fullHash } = result
+
+	// Count messages that will be removed
+	const currentIndex = chatMessages.findIndex((msg) => msg.ts === message.ts)
+	const messagesToRemove = chatMessages.length - currentIndex - 1
+
+	logs.info("Preparing to restore checkpoint", "checkpoint", { fullHash, messagesToRemove })
+
+	// Send request to extension to create ask message and handle approval
+	const confirmLines = [
+		`**Warning:** This will revert to checkpoint ${fullHash}`,
+		"",
+		"This action will:",
+		`  - Perform a git hard reset (all uncommitted changes will be lost)`,
+		`  - Remove ${messagesToRemove} message${messagesToRemove === 1 ? "" : "s"} from the conversation`,
+		`  - Revert to state from ${formatTimestamp(message.ts)}`,
+		"",
+		"**This cannot be undone.**",
+	]
+
+	await sendMessage({
+		type: "requestCheckpointRestoreApproval",
+		payload: {
+			commitHash: fullHash,
+			checkpointTs: message.ts,
+			messagesToRemove: messagesToRemove,
+			confirmationText: confirmLines.join("\n"),
+		},
+	})
+
+	logs.info("Sent checkpoint restore approval request to extension", "checkpoint", { fullHash })
+}
+
+/**
+ * Argument provider for checkpoint hashes
+ */
+async function provideCheckpointHashes(context: ArgumentProviderContext): Promise<ArgumentSuggestion[]> {
+	const chatMessages = context.commandContext?.chatMessages || []
+	const checkpoints = getCheckpointMessages(chatMessages)
+
+	logs.debug("Providing checkpoint hash suggestions", "checkpoint", { count: checkpoints.length })
+
+	if (checkpoints.length === 0) {
+		logs.info("No checkpoints available for suggestions", "checkpoint")
+		return [
+			{
+				value: "",
+				title: "No checkpoints available",
+				matchScore: 0,
+				highlightedValue: "",
+				loading: false,
+			},
+		]
+	}
+
+	return checkpoints.map((cp, index) => {
+		const hash = cp.text || ""
+		const timestamp = formatTimestamp(cp.ts)
+		const isSuppressed = cp.metadata?.suppressMessage === true
+		const label = isSuppressed ? " (auto-saved)" : ""
+
+		return {
+			value: hash,
+			title: `${hash} - ${timestamp}${label}`,
+			description: hash,
+			matchScore: 100 - index, // Recent first
+			highlightedValue: hash,
+		}
+	})
+}
+
+/**
+ * Argument provider for subcommands
+ */
+async function provideSubcommands(context: ArgumentProviderContext): Promise<ArgumentSuggestion[]> {
+	const subcommands = [
+		{ value: "list", description: "List all available checkpoints" },
+		{ value: "restore", description: "Revert to a checkpoint" },
+	]
+
+	const query = context.partialInput.toLowerCase()
+
+	logs.debug("Providing subcommand suggestions", "checkpoint", { query, subcommandCount: subcommands.length })
+
+	return subcommands
+		.map((cmd) => {
+			let matchScore = 0
+			if (!query) {
+				matchScore = 100
+			} else if (cmd.value === query) {
+				matchScore = 100
+			} else if (cmd.value.startsWith(query)) {
+				matchScore = 90
+			} else if (cmd.value.includes(query)) {
+				matchScore = 70
+			}
+
+			return {
+				value: cmd.value,
+				title: cmd.value,
+				description: cmd.description,
+				matchScore,
+				highlightedValue: cmd.value,
+			}
+		})
+		.filter((s) => s.matchScore > 0)
+		.sort((a, b) => b.matchScore - a.matchScore)
+}
+
+export const checkpointCommand: Command = {
+	name: "checkpoint",
+	aliases: ["cp"],
+	description: "Manage and revert to saved checkpoints",
+	usage: "/checkpoint <list|restore> [hash]",
+	examples: ["/checkpoint list", "/checkpoint restore 41db173a"],
+	category: "chat",
+	priority: 7,
+	arguments: [
+		{
+			name: "subcommand",
+			description: "The action to perform (list, restore)",
+			required: false,
+			provider: provideSubcommands,
+		},
+		{
+			name: "hash",
+			description: "Checkpoint hash (full 40-character git hash)",
+			required: false,
+			provider: provideCheckpointHashes,
+		},
+	],
+	handler: async (context) => {
+		const { args, addMessage } = context
+
+		// No subcommand - show help
+		if (args.length === 0 || !args[0]) {
+			logs.info("Showing checkpoint help", "checkpoint")
+			addMessage({
+				id: Date.now().toString(),
+				type: "system",
+				content: [
+					"**Checkpoint Management**",
+					"",
+					"**Usage:** /checkpoint <command> [hash]",
+					"",
+					"**Commands:**",
+					"  list           List all available checkpoints",
+					"  restore <hash> Revert to a checkpoint (destructive)",
+					"",
+					"**Examples:**",
+					"  /checkpoint list",
+					"  /checkpoint restore 00d185d5020969752bc9ae40823b9d6a723696e2",
+					"",
+					"**Note:** Hash must be the full 40-character git commit hash.",
+				].join("\n"),
+				ts: Date.now(),
+			})
+			return
+		}
+
+		const subcommand = args[0].toLowerCase()
+		const hash = args[1]
+
+		logs.info("Executing checkpoint command", "checkpoint", { subcommand, hash: hash || null })
+
+		switch (subcommand) {
+			case "list":
+				logs.debug("Handling checkpoint list command", "checkpoint")
+				await handleList(context)
+				break
+
+			case "restore":
+				if (!hash) {
+					logs.warn("Hash required for restore command", "checkpoint")
+					addMessage({
+						id: Date.now().toString(),
+						type: "error",
+						content: "Hash required. Usage: /checkpoint restore <hash>",
+						ts: Date.now(),
+					})
+					return
+				}
+				logs.debug("Handling checkpoint restore command", "checkpoint", { hash })
+				await handleRestore(context, hash)
+				break
+
+			default:
+				logs.warn("Unknown checkpoint subcommand", "checkpoint", { subcommand })
+				addMessage({
+					id: Date.now().toString(),
+					type: "error",
+					content: `Unknown command "${subcommand}". Available: list, restore`,
+					ts: Date.now(),
+				})
+		}
+	},
+}

+ 3 - 1
cli/src/commands/core/types.ts

@@ -2,7 +2,7 @@
  * Command system type definitions
  */
 
-import type { RouterModels } from "../../types/messages.js"
+import type { ExtensionMessage, RouterModels } from "../../types/messages.js"
 import type { CLIConfig, ProviderConfig } from "../../config/types.js"
 import type { ProfileData, BalanceData } from "../../state/atoms/profile.js"
 import type { TaskHistoryData, TaskHistoryFilters } from "../../state/atoms/taskHistory.js"
@@ -70,6 +70,7 @@ export interface CommandContext {
 	previousTaskHistoryPage: () => Promise<TaskHistoryData>
 	sendWebviewMessage: (message: any) => Promise<void>
 	refreshTerminal: () => Promise<void>
+	chatMessages: ExtensionMessage[]
 }
 
 export type CommandHandler = (context: CommandContext) => Promise<void> | void
@@ -132,6 +133,7 @@ export interface ArgumentProviderContext {
 		updateProviderModel: (modelId: string) => Promise<void>
 		refreshRouterModels: () => Promise<void>
 		taskHistoryData: TaskHistoryData | null
+		chatMessages: ExtensionMessage[]
 	}
 }
 

+ 2 - 0
cli/src/commands/index.ts

@@ -18,6 +18,7 @@ import { teamsCommand } from "./teams.js"
 import { configCommand } from "./config.js"
 import { tasksCommand } from "./tasks.js"
 import { themeCommand } from "./theme.js"
+import { checkpointCommand } from "./checkpoint.js"
 
 /**
  * Initialize all commands
@@ -35,4 +36,5 @@ export function initializeCommands(): void {
 	commandRegistry.register(configCommand)
 	commandRegistry.register(tasksCommand)
 	commandRegistry.register(themeCommand)
+	commandRegistry.register(checkpointCommand)
 }

+ 0 - 2
cli/src/commands/tasks.ts

@@ -271,8 +271,6 @@ async function changePage(context: any, pageNum: string): Promise<void> {
  */
 async function nextPage(context: any): Promise<void> {
 	const { taskHistoryData, nextTaskHistoryPage, addMessage } = context
-	const now = Date.now()
-
 	if (!taskHistoryData) {
 		addMessage({
 			...generateMessage(),

+ 253 - 0
cli/src/config/__tests__/env-overrides.test.ts

@@ -0,0 +1,253 @@
+import { describe, it, expect, beforeEach, afterEach } from "vitest"
+import { applyEnvOverrides, PROVIDER_ENV_VAR, KILOCODE_PREFIX, KILO_PREFIX } from "../env-overrides.js"
+import type { CLIConfig } from "../types.js"
+
+describe("env-overrides", () => {
+	const originalEnv = process.env
+	let testConfig: CLIConfig
+
+	beforeEach(() => {
+		// Reset environment variables before each test
+		process.env = { ...originalEnv }
+
+		// Clear any KILOCODE_* or KILO_* environment variables to ensure clean test state
+		for (const key of Object.keys(process.env)) {
+			if (key.startsWith(KILOCODE_PREFIX) || key.startsWith(KILO_PREFIX)) {
+				delete process.env[key]
+			}
+		}
+
+		// Create a test config
+		testConfig = {
+			version: "1.0.0",
+			mode: "code",
+			telemetry: true,
+			provider: "default",
+			providers: [
+				{
+					id: "default",
+					provider: "kilocode",
+					kilocodeToken: "test-token",
+					kilocodeModel: "anthropic/claude-sonnet-4.5",
+					kilocodeOrganizationId: "original-org-id",
+				},
+				{
+					id: "anthropic-provider",
+					provider: "anthropic",
+					apiKey: "test-key",
+					apiModelId: "claude-3-5-sonnet-20241022",
+				},
+			],
+			autoApproval: {
+				enabled: true,
+			},
+			theme: "dark",
+			customThemes: {},
+		}
+	})
+
+	afterEach(() => {
+		// Restore original environment
+		process.env = originalEnv
+	})
+
+	describe("KILO_PROVIDER override", () => {
+		it("should override provider when KILO_PROVIDER is set and provider exists", () => {
+			process.env[PROVIDER_ENV_VAR] = "anthropic-provider"
+
+			const result = applyEnvOverrides(testConfig)
+
+			expect(result.provider).toBe("anthropic-provider")
+		})
+
+		it("should not override provider when KILO_PROVIDER provider does not exist", () => {
+			process.env[PROVIDER_ENV_VAR] = "nonexistent-provider"
+
+			const result = applyEnvOverrides(testConfig)
+
+			expect(result.provider).toBe("default")
+		})
+
+		it("should not override provider when KILO_PROVIDER is empty", () => {
+			process.env[PROVIDER_ENV_VAR] = ""
+
+			const result = applyEnvOverrides(testConfig)
+
+			expect(result.provider).toBe("default")
+		})
+	})
+
+	describe("KILOCODE_* overrides for kilocode provider", () => {
+		it("should transform KILOCODE_MODEL to kilocodeModel", () => {
+			process.env[`${KILOCODE_PREFIX}MODEL`] = "anthropic/claude-opus-4.0"
+
+			const result = applyEnvOverrides(testConfig)
+
+			const provider = result.providers.find((p) => p.id === "default")
+			expect(provider?.kilocodeModel).toBe("anthropic/claude-opus-4.0")
+		})
+
+		it("should transform KILOCODE_ORGANIZATION_ID to kilocodeOrganizationId", () => {
+			process.env[`${KILOCODE_PREFIX}ORGANIZATION_ID`] = "new-org-id"
+
+			const result = applyEnvOverrides(testConfig)
+
+			const provider = result.providers.find((p) => p.id === "default")
+			expect(provider?.kilocodeOrganizationId).toBe("new-org-id")
+		})
+
+		it("should handle multiple KILOCODE_* overrides", () => {
+			process.env[`${KILOCODE_PREFIX}MODEL`] = "anthropic/claude-opus-4.0"
+			process.env[`${KILOCODE_PREFIX}ORGANIZATION_ID`] = "new-org-id"
+			process.env[`${KILOCODE_PREFIX}TOKEN`] = "new-token"
+
+			const result = applyEnvOverrides(testConfig)
+
+			const provider = result.providers.find((p) => p.id === "default")
+			expect(provider?.kilocodeModel).toBe("anthropic/claude-opus-4.0")
+			expect(provider?.kilocodeOrganizationId).toBe("new-org-id")
+			expect(provider?.kilocodeToken).toBe("new-token")
+		})
+	})
+
+	describe("KILO_* overrides for non-kilocode providers", () => {
+		it("should transform KILO_API_KEY to apiKey for non-kilocode provider", () => {
+			process.env[PROVIDER_ENV_VAR] = "anthropic-provider"
+			process.env[`${KILO_PREFIX}API_KEY`] = "new-key"
+
+			const result = applyEnvOverrides(testConfig)
+
+			expect(result.provider).toBe("anthropic-provider")
+			const provider = result.providers.find((p) => p.id === "anthropic-provider")
+			expect(provider?.apiKey).toBe("new-key")
+		})
+
+		it("should transform KILO_API_MODEL_ID to apiModelId", () => {
+			process.env[PROVIDER_ENV_VAR] = "anthropic-provider"
+			process.env[`${KILO_PREFIX}API_MODEL_ID`] = "claude-3-opus-20240229"
+
+			const result = applyEnvOverrides(testConfig)
+
+			const provider = result.providers.find((p) => p.id === "anthropic-provider")
+			expect(provider?.apiModelId).toBe("claude-3-opus-20240229")
+		})
+
+		it("should transform KILO_BASE_URL to baseUrl", () => {
+			process.env[PROVIDER_ENV_VAR] = "anthropic-provider"
+			process.env[`${KILO_PREFIX}BASE_URL`] = "https://api.example.com"
+
+			const result = applyEnvOverrides(testConfig)
+
+			const provider = result.providers.find((p) => p.id === "anthropic-provider")
+			expect(provider?.baseUrl).toBe("https://api.example.com")
+		})
+
+		it("should not apply KILO_* overrides to kilocode provider", () => {
+			process.env[PROVIDER_ENV_VAR] = "kilocode"
+			process.env[`${KILO_PREFIX}API_KEY`] = "should-not-apply"
+
+			const result = applyEnvOverrides(testConfig)
+
+			const provider = result.providers.find((p) => p.id === "default")
+			expect(provider?.apiKey).toBeUndefined()
+		})
+
+		it("should not apply KILOCODE_* overrides to non-kilocode provider", () => {
+			process.env[PROVIDER_ENV_VAR] = "anthropic-provider"
+			process.env[`${KILOCODE_PREFIX}MODEL`] = "should-not-apply"
+
+			const result = applyEnvOverrides(testConfig)
+
+			const provider = result.providers.find((p) => p.id === "anthropic-provider")
+			expect(provider?.kilocodeModel).toBeUndefined()
+		})
+	})
+
+	describe("Combined overrides", () => {
+		it("should apply both provider and field overrides together for non-kilocode provider", () => {
+			process.env[PROVIDER_ENV_VAR] = "anthropic-provider"
+			process.env[`${KILO_PREFIX}API_MODEL_ID`] = "claude-3-opus-20240229"
+			process.env[`${KILO_PREFIX}API_KEY`] = "new-key"
+
+			const result = applyEnvOverrides(testConfig)
+
+			expect(result.provider).toBe("anthropic-provider")
+			const provider = result.providers.find((p) => p.id === "anthropic-provider")
+			expect(provider?.apiModelId).toBe("claude-3-opus-20240229")
+			expect(provider?.apiKey).toBe("new-key")
+		})
+
+		it("should apply both provider and field overrides together for kilocode provider", () => {
+			process.env[PROVIDER_ENV_VAR] = "default"
+			process.env[`${KILOCODE_PREFIX}MODEL`] = "anthropic/claude-opus-4.0"
+			process.env[`${KILOCODE_PREFIX}ORGANIZATION_ID`] = "new-org-id"
+
+			const result = applyEnvOverrides(testConfig)
+
+			expect(result.provider).toBe("default")
+			const provider = result.providers.find((p) => p.id === "default")
+			expect(provider?.kilocodeModel).toBe("anthropic/claude-opus-4.0")
+			expect(provider?.kilocodeOrganizationId).toBe("new-org-id")
+		})
+	})
+
+	describe("Edge cases", () => {
+		it("should handle empty config providers array", () => {
+			testConfig.providers = []
+
+			const result = applyEnvOverrides(testConfig)
+
+			expect(result.providers).toEqual([])
+		})
+
+		it("should handle config with no current provider", () => {
+			testConfig.provider = "nonexistent"
+
+			const result = applyEnvOverrides(testConfig)
+
+			expect(result).toEqual(testConfig)
+		})
+
+		it("should handle empty string override values for KILOCODE_*", () => {
+			process.env[`${KILOCODE_PREFIX}MODEL`] = ""
+
+			const result = applyEnvOverrides(testConfig)
+
+			// Empty strings should not trigger overrides
+			const provider = result.providers.find((p) => p.id === "default")
+			expect(provider?.kilocodeModel).toBe("anthropic/claude-sonnet-4.5")
+		})
+
+		it("should handle empty string override values for KILO_*", () => {
+			process.env[PROVIDER_ENV_VAR] = "anthropic-provider"
+			process.env[`${KILO_PREFIX}API_KEY`] = ""
+
+			const result = applyEnvOverrides(testConfig)
+
+			// Empty strings should not trigger overrides
+			const provider = result.providers.find((p) => p.id === "anthropic-provider")
+			expect(provider?.apiKey).toBe("test-key")
+		})
+
+		it("should ignore KILOCODE_ with no field name", () => {
+			process.env[KILOCODE_PREFIX.slice(0, -1)] = "value"
+
+			const result = applyEnvOverrides(testConfig)
+
+			// Should not modify anything
+			expect(result).toEqual(testConfig)
+		})
+
+		it("should ignore KILO_PROVIDER since it's handled separately", () => {
+			process.env[PROVIDER_ENV_VAR] = "anthropic-provider"
+
+			const result = applyEnvOverrides(testConfig)
+
+			// KILO_PROVIDER should change the provider but not add a 'provider' field
+			expect(result.provider).toBe("anthropic-provider")
+
+			const provider = result.providers.find((p) => p.id === "anthropic-provider")
+			expect(provider?.provider).toBe("anthropic") // Original value
+		})
+	})
+})

+ 339 - 0
cli/src/config/__tests__/openConfig.test.ts

@@ -0,0 +1,339 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
+import { spawn } from "child_process"
+import { platform } from "os"
+import openConfigFile from "../openConfig.js"
+import * as configModule from "../index.js"
+import { EventEmitter } from "events"
+
+// Mock dependencies
+vi.mock("child_process")
+vi.mock("os")
+vi.mock("../index.js", () => ({
+	ensureConfigDir: vi.fn(),
+	configExists: vi.fn(),
+	saveConfig: vi.fn(),
+	getConfigPath: vi.fn(),
+	DEFAULT_CONFIG: {},
+}))
+
+describe("openConfigFile", () => {
+	const mockConfigPath = "/home/user/.config/kilocode/config.json"
+	let originalEnv: NodeJS.ProcessEnv
+	let consoleLogSpy: ReturnType<typeof vi.spyOn>
+	let consoleErrorSpy: ReturnType<typeof vi.spyOn>
+	let processExitSpy: any
+
+	beforeEach(() => {
+		vi.clearAllMocks()
+		originalEnv = { ...process.env }
+
+		// Setup default mocks
+		vi.mocked(configModule.ensureConfigDir).mockResolvedValue(undefined)
+		vi.mocked(configModule.configExists).mockResolvedValue(true)
+		vi.mocked(configModule.getConfigPath).mockResolvedValue(mockConfigPath)
+
+		// Mock console methods
+		consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {})
+		consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
+		processExitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as any)
+	})
+
+	afterEach(() => {
+		process.env = originalEnv
+		vi.restoreAllMocks()
+	})
+
+	const createMockProcess = () => {
+		const mockProcess = new EventEmitter() as any
+		mockProcess.stdio = { inherit: true }
+		return mockProcess
+	}
+
+	describe("Linux platform", () => {
+		beforeEach(() => {
+			vi.mocked(platform).mockReturnValue("linux")
+			delete process.env.EDITOR
+			delete process.env.VISUAL
+		})
+
+		it("should use xdg-open by default on Linux", async () => {
+			const mockProcess = createMockProcess()
+			vi.mocked(spawn).mockReturnValue(mockProcess)
+
+			const promise = openConfigFile()
+
+			// Wait for async setup to complete
+			await new Promise((resolve) => setImmediate(resolve))
+
+			expect(spawn).toHaveBeenCalledWith("xdg-open", [mockConfigPath], {
+				stdio: "inherit",
+			})
+
+			// Simulate successful exit
+			mockProcess.emit("exit", 0)
+
+			await promise
+		})
+
+		it("should fallback to nano when xdg-open fails on Linux", async () => {
+			const xdgProcess = createMockProcess()
+			const nanoProcess = createMockProcess()
+
+			vi.mocked(spawn).mockReturnValueOnce(xdgProcess).mockReturnValueOnce(nanoProcess)
+
+			const promise = openConfigFile()
+
+			// Wait for async setup to complete
+			await new Promise((resolve) => setImmediate(resolve))
+
+			// Simulate xdg-open error
+			const error = new Error("xdg-open: command not found")
+			xdgProcess.emit("error", error)
+
+			// Wait for fallback to be triggered
+			await new Promise((resolve) => setImmediate(resolve))
+
+			expect(consoleLogSpy).toHaveBeenCalledWith("xdg-open failed, trying nano as fallback...")
+			expect(spawn).toHaveBeenNthCalledWith(2, "nano", [mockConfigPath], {
+				stdio: "inherit",
+			})
+
+			// Simulate nano successful exit
+			nanoProcess.emit("exit", 0)
+
+			await promise
+		})
+
+		it("should report error when both xdg-open and nano fail", async () => {
+			const xdgProcess = createMockProcess()
+			const nanoProcess = createMockProcess()
+
+			vi.mocked(spawn).mockReturnValueOnce(xdgProcess).mockReturnValueOnce(nanoProcess)
+
+			const promise = openConfigFile()
+
+			// Wait for async setup to complete
+			await new Promise((resolve) => setImmediate(resolve))
+
+			// Simulate xdg-open error
+			const xdgError = new Error("xdg-open: command not found")
+			xdgProcess.emit("error", xdgError)
+
+			// Wait for fallback to be triggered
+			await new Promise((resolve) => setImmediate(resolve))
+
+			// Simulate nano error
+			const nanoError = new Error("nano: command not found")
+			nanoProcess.emit("error", nanoError)
+
+			await promise
+
+			expect(consoleErrorSpy).toHaveBeenCalledWith(`Failed to open editor: ${xdgError.message}`)
+			expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Nano fallback also failed"))
+			expect(processExitSpy).toHaveBeenCalledWith(1)
+		})
+
+		it("should not fallback to nano when EDITOR is set", async () => {
+			process.env.EDITOR = "vim"
+
+			const mockProcess = createMockProcess()
+			vi.mocked(spawn).mockReturnValue(mockProcess)
+
+			const promise = openConfigFile()
+
+			// Wait for async setup to complete
+			await new Promise((resolve) => setImmediate(resolve))
+
+			expect(spawn).toHaveBeenCalledWith("vim", [mockConfigPath], {
+				stdio: "inherit",
+			})
+
+			// Simulate error with custom editor
+			const error = new Error("vim: command not found")
+			mockProcess.emit("error", error)
+
+			await promise
+
+			expect(spawn).toHaveBeenCalledTimes(1)
+			expect(consoleErrorSpy).toHaveBeenCalledWith(`Failed to open editor: ${error.message}`)
+			expect(consoleErrorSpy).not.toHaveBeenCalledWith(expect.stringContaining("Nano fallback"))
+			expect(processExitSpy).toHaveBeenCalledWith(1)
+		})
+	})
+
+	describe("macOS platform", () => {
+		beforeEach(() => {
+			vi.mocked(platform).mockReturnValue("darwin")
+			delete process.env.EDITOR
+			delete process.env.VISUAL
+		})
+
+		it("should use open with -t flag on macOS", async () => {
+			const mockProcess = createMockProcess()
+			vi.mocked(spawn).mockReturnValue(mockProcess)
+
+			const promise = openConfigFile()
+
+			// Wait for async setup to complete
+			await new Promise((resolve) => setImmediate(resolve))
+
+			expect(spawn).toHaveBeenCalledWith("open", ["-t", mockConfigPath], {
+				stdio: "inherit",
+			})
+
+			mockProcess.emit("exit", 0)
+			await promise
+		})
+
+		it("should not fallback to nano on macOS", async () => {
+			const mockProcess = createMockProcess()
+			vi.mocked(spawn).mockReturnValue(mockProcess)
+
+			const promise = openConfigFile()
+
+			// Wait for async setup to complete
+			await new Promise((resolve) => setImmediate(resolve))
+
+			const error = new Error("open: command not found")
+			mockProcess.emit("error", error)
+
+			await promise
+
+			expect(spawn).toHaveBeenCalledTimes(1)
+			expect(consoleErrorSpy).not.toHaveBeenCalledWith(expect.stringContaining("nano"))
+			expect(processExitSpy).toHaveBeenCalledWith(1)
+		})
+	})
+
+	describe("Windows platform", () => {
+		beforeEach(() => {
+			vi.mocked(platform).mockReturnValue("win32")
+			delete process.env.EDITOR
+			delete process.env.VISUAL
+		})
+
+		it("should use cmd /c start on Windows", async () => {
+			const mockProcess = createMockProcess()
+			vi.mocked(spawn).mockReturnValue(mockProcess)
+
+			const promise = openConfigFile()
+
+			// Wait for async setup to complete
+			await new Promise((resolve) => setImmediate(resolve))
+
+			expect(spawn).toHaveBeenCalledWith("cmd", ["/c", "start", "", mockConfigPath], {
+				stdio: "inherit",
+			})
+
+			mockProcess.emit("exit", 0)
+			await promise
+		})
+
+		it("should not fallback to nano on Windows", async () => {
+			const mockProcess = createMockProcess()
+			vi.mocked(spawn).mockReturnValue(mockProcess)
+
+			const promise = openConfigFile()
+
+			// Wait for async setup to complete
+			await new Promise((resolve) => setImmediate(resolve))
+
+			const error = new Error("cmd: command not found")
+			mockProcess.emit("error", error)
+
+			await promise
+
+			expect(spawn).toHaveBeenCalledTimes(1)
+			expect(consoleErrorSpy).not.toHaveBeenCalledWith(expect.stringContaining("nano"))
+			expect(processExitSpy).toHaveBeenCalledWith(1)
+		})
+	})
+
+	describe("Config file creation", () => {
+		it("should create default config if it doesn't exist", async () => {
+			vi.mocked(platform).mockReturnValue("linux")
+			vi.mocked(configModule.configExists).mockResolvedValue(false)
+
+			const mockProcess = createMockProcess()
+			vi.mocked(spawn).mockReturnValue(mockProcess)
+
+			const promise = openConfigFile()
+
+			// Wait for async setup to complete
+			await new Promise((resolve) => setImmediate(resolve))
+
+			expect(consoleLogSpy).toHaveBeenCalledWith("Config file not found. Creating default configuration...")
+			expect(configModule.saveConfig).toHaveBeenCalledWith(configModule.DEFAULT_CONFIG, true)
+			expect(consoleLogSpy).toHaveBeenCalledWith("Default configuration created.")
+
+			mockProcess.emit("exit", 0)
+			await promise
+		})
+	})
+
+	describe("EDITOR environment variable", () => {
+		beforeEach(() => {
+			vi.mocked(platform).mockReturnValue("linux")
+		})
+
+		it("should use EDITOR environment variable when set", async () => {
+			process.env.EDITOR = "emacs"
+
+			const mockProcess = createMockProcess()
+			vi.mocked(spawn).mockReturnValue(mockProcess)
+
+			const promise = openConfigFile()
+
+			// Wait for async setup to complete
+			await new Promise((resolve) => setImmediate(resolve))
+
+			expect(spawn).toHaveBeenCalledWith("emacs", [mockConfigPath], {
+				stdio: "inherit",
+			})
+
+			mockProcess.emit("exit", 0)
+			await promise
+		})
+
+		it("should use VISUAL environment variable when set", async () => {
+			delete process.env.EDITOR
+			delete process.env.VISUAL
+			process.env.VISUAL = "code"
+
+			const mockProcess = createMockProcess()
+			vi.mocked(spawn).mockReturnValue(mockProcess)
+
+			const promise = openConfigFile()
+
+			// Wait for async setup to complete
+			await new Promise((resolve) => setImmediate(resolve))
+
+			expect(spawn).toHaveBeenCalledWith("code", [mockConfigPath], {
+				stdio: "inherit",
+			})
+
+			mockProcess.emit("exit", 0)
+			await promise
+		})
+
+		it("should prefer EDITOR over VISUAL", async () => {
+			process.env.EDITOR = "vim"
+			process.env.VISUAL = "code"
+
+			const mockProcess = createMockProcess()
+			vi.mocked(spawn).mockReturnValue(mockProcess)
+
+			const promise = openConfigFile()
+
+			// Wait for async setup to complete
+			await new Promise((resolve) => setImmediate(resolve))
+
+			expect(spawn).toHaveBeenCalledWith("vim", [mockConfigPath], {
+				stdio: "inherit",
+			})
+
+			mockProcess.emit("exit", 0)
+			await promise
+		})
+	})
+})

+ 142 - 0
cli/src/config/env-overrides.ts

@@ -0,0 +1,142 @@
+import type { CLIConfig } from "./types.js"
+import { logs } from "../services/logs.js"
+
+/**
+ * Environment variable name for provider selection
+ */
+export const PROVIDER_ENV_VAR = "KILO_PROVIDER"
+
+/**
+ * Environment variable prefix for Kilocode provider
+ */
+export const KILOCODE_PREFIX = "KILOCODE_"
+
+/**
+ * Environment variable prefix for other providers
+ */
+export const KILO_PREFIX = "KILO_"
+
+const specificEnvVars = new Set([PROVIDER_ENV_VAR])
+
+/**
+ * Apply environment variable overrides to the config
+ * Overrides the current provider's settings based on environment variables
+ *
+ * Environment variables:
+ * - KILO_PROVIDER: Override the active provider ID
+ * - For Kilocode provider: KILOCODE_<FIELD_NAME> (e.g., KILOCODE_MODEL → kilocodeModel)
+ *   Examples:
+ *   - KILOCODE_MODEL → kilocodeModel
+ *   - KILOCODE_ORGANIZATION_ID → kilocodeOrganizationId
+ * - For other providers: KILO_<FIELD_NAME> (e.g., KILO_API_KEY → apiKey)
+ *   Examples:
+ *   - KILO_API_KEY → apiKey
+ *   - KILO_BASE_URL → baseUrl
+ *   - KILO_API_MODEL_ID → apiModelId
+ *
+ * @param config The config to apply overrides to
+ * @returns The config with environment variable overrides applied
+ */
+export function applyEnvOverrides(config: CLIConfig): CLIConfig {
+	const overriddenConfig = { ...config }
+
+	// Override provider if KILO_PROVIDER is set
+	const envProvider = process.env[PROVIDER_ENV_VAR]
+
+	if (envProvider) {
+		// Check if the provider exists in the config
+		const providerExists = config.providers.some((p) => p.id === envProvider)
+
+		if (providerExists) {
+			overriddenConfig.provider = envProvider
+
+			logs.info(`Config override: provider set to "${envProvider}" from ${PROVIDER_ENV_VAR}`, "EnvOverrides")
+		} else {
+			logs.warn(
+				`Config override ignored: provider "${envProvider}" from ${PROVIDER_ENV_VAR} not found in config`,
+				"EnvOverrides",
+			)
+		}
+	}
+
+	// Get the current provider (after potential provider override)
+	const currentProvider = overriddenConfig.providers.find((p) => p.id === overriddenConfig.provider)
+
+	if (!currentProvider) {
+		// No valid provider, return config as-is
+		return overriddenConfig
+	}
+
+	// Find all environment variable overrides for the current provider
+	const overrideFields = getProviderOverrideFields(currentProvider.provider)
+
+	if (overrideFields.length > 0) {
+		// Create a new providers array with the updated provider
+		overriddenConfig.providers = overriddenConfig.providers.map((p) => {
+			if (p.id === currentProvider.id) {
+				const updatedProvider = { ...p }
+
+				// Apply each override
+				for (const { fieldName, value } of overrideFields) {
+					updatedProvider[fieldName] = value
+
+					logs.info(
+						`Config override: ${fieldName} set to "${value}" for provider "${currentProvider.id}"`,
+						"EnvOverrides",
+					)
+				}
+
+				return updatedProvider
+			}
+
+			return p
+		})
+	}
+
+	return overriddenConfig
+}
+
+/**
+ * Convert snake_case or SCREAMING_SNAKE_CASE to camelCase
+ * Examples:
+ * - API_KEY → apiKey
+ * - BASE_URL → baseUrl
+ * - API_MODEL_ID → apiModelId
+ * - ORGANIZATION_ID → organizationId
+ */
+function snakeToCamelCase(str: string): string {
+	return str.toLowerCase().replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
+}
+
+/**
+ * Get all environment variable overrides for the current provider
+ * - For Kilocode provider: looks for KILOCODE_* vars and transforms to kilocodeXyz
+ * - For other providers: looks for KILO_* vars (excluding KILO_PROVIDER) and transforms to xyzAbc
+ * Returns an array of { fieldName, value } objects
+ */
+function getProviderOverrideFields(provider: string): Array<{ fieldName: string; value: string }> {
+	const overrides: Array<{ fieldName: string; value: string }> = []
+
+	if (provider === "kilocode") {
+		// For Kilocode provider: KILOCODE_XYZ → kilocodeXyz
+		for (const [key, value] of Object.entries(process.env)) {
+			if (key.startsWith(KILOCODE_PREFIX) && value) {
+				overrides.push({ fieldName: snakeToCamelCase(key), value })
+			}
+		}
+	} else {
+		// For other providers: KILO_XYZ_ABC → xyzAbc
+		for (const [key, value] of Object.entries(process.env)) {
+			if (key.startsWith(KILO_PREFIX) && !specificEnvVars.has(key) && value) {
+				const remainder = key.substring(KILO_PREFIX.length)
+
+				if (remainder) {
+					const fieldName = snakeToCamelCase(remainder)
+					overrides.push({ fieldName, value })
+				}
+			}
+		}
+	}
+
+	return overrides
+}

+ 30 - 4
cli/src/config/openConfig.ts

@@ -48,16 +48,42 @@ export default async function openConfigFile() {
 			}
 		}
 
+		// Track if we should try nano fallback on Linux
+		const shouldTryNanoFallback = !editor && platform() === "linux" && editorCommand === "xdg-open"
+
 		// Spawn the editor process
 		const editorProcess = spawn(editorCommand, editorArgs, {
 			stdio: "inherit",
 		})
 
 		editorProcess.on("error", (error) => {
-			console.error(`Failed to open editor: ${error.message}`)
-			console.error(`Tried to run: ${editorCommand} ${editorArgs.join(" ")}`)
-			console.error(`\nYou can manually edit the config file at: ${configPath}`)
-			process.exit(1)
+			// On Linux, if xdg-open fails and no EDITOR is set, try nano as fallback
+			if (shouldTryNanoFallback) {
+				console.log(`xdg-open failed, trying nano as fallback...`)
+				const nanoProcess = spawn("nano", [configPath], {
+					stdio: "inherit",
+				})
+
+				nanoProcess.on("error", (nanoError) => {
+					console.error(`Failed to open editor: ${error.message}`)
+					console.error(`Tried to run: ${editorCommand} ${editorArgs.join(" ")}`)
+					console.error(`Nano fallback also failed: ${nanoError.message}`)
+					console.error(`\nYou can manually edit the config file at: ${configPath}`)
+					process.exit(1)
+				})
+
+				nanoProcess.on("exit", (code) => {
+					if (code !== 0 && code !== null) {
+						console.error(`Nano exited with code ${code}`)
+						console.error(`Config file location: ${configPath}`)
+					}
+				})
+			} else {
+				console.error(`Failed to open editor: ${error.message}`)
+				console.error(`Tried to run: ${editorCommand} ${editorArgs.join(" ")}`)
+				console.error(`\nYou can manually edit the config file at: ${configPath}`)
+				process.exit(1)
+			}
 		})
 
 		editorProcess.on("exit", (code) => {

+ 64 - 0
cli/src/config/schema.json

@@ -257,6 +257,7 @@
 						"qwen-code",
 						"gemini-cli",
 						"zai",
+						"minimax",
 						"unbound",
 						"requesty",
 						"roo",
@@ -1207,6 +1208,27 @@
 						}
 					}
 				},
+				{
+					"if": {
+						"properties": { "provider": { "const": "minimax" } }
+					},
+					"then": {
+						"properties": {
+							"minimaxBaseUrl": {
+								"type": "string",
+								"description": "MiniMax base URL"
+							},
+							"minimaxApiKey": {
+								"type": "string",
+								"description": "MiniMax API key"
+							},
+							"apiModelId": {
+								"type": "string",
+								"description": "MiniMax model ID"
+							}
+						}
+					}
+				},
 				{
 					"if": {
 						"properties": { "provider": { "const": "doubao" } }
@@ -1294,6 +1316,48 @@
 						}
 					}
 				},
+				{
+					"if": {
+						"properties": {
+							"provider": { "const": "minimax" },
+							"minimaxBaseUrl": { "type": "string", "minLength": 1 }
+						},
+						"required": ["minimaxBaseUrl"]
+					},
+					"then": {
+						"properties": {
+							"minimaxBaseUrl": { "minLength": 1 }
+						}
+					}
+				},
+				{
+					"if": {
+						"properties": {
+							"provider": { "const": "minimax" },
+							"minimaxApiKey": { "type": "string", "minLength": 1 }
+						},
+						"required": ["minimaxApiKey"]
+					},
+					"then": {
+						"properties": {
+							"minimaxApiKey": { "minLength": 10 }
+						}
+					}
+				},
+				{
+					"if": {
+						"properties": {
+							"provider": { "const": "minimax" },
+							"apiModelId": { "type": "string", "minLength": 1 }
+						},
+						"required": ["apiModelId"]
+					},
+					"then": {
+						"properties": {
+							"apiModelId": { "minLength": 1 }
+						}
+					}
+				},
 				{
 					"if": {
 						"properties": { "provider": { "const": "chutes" } }

+ 2 - 0
cli/src/constants/keyboard/keyCodes.ts

@@ -11,6 +11,8 @@ export const CHAR_CODE_ENTER = 13
 export const CHAR_CODE_CTRL_U = 21
 export const CHAR_CODE_ESC = 27
 export const CHAR_CODE_SPACE = 32
+export const CHAR_CODE_EXCLAMATION = 33 // ! character
+export const CHAR_CODE_DIGIT_1 = 49 // 1 character
 export const CHAR_CODE_DELETE = 127
 
 // Kitty protocol specific key codes

+ 1 - 0
cli/src/constants/providers/__tests__/models.test.ts

@@ -42,6 +42,7 @@ describe("Static Provider Models", () => {
 			"cerebras",
 			"sambanova",
 			"zai",
+			"minimax",
 			"fireworks",
 			"featherless",
 			"roo",

+ 1 - 0
cli/src/constants/providers/labels.ts

@@ -36,6 +36,7 @@ export const PROVIDER_LABELS: Record<ProviderName, string> = {
 	"qwen-code": "Qwen Code",
 	"gemini-cli": "Gemini CLI",
 	zai: "Zai",
+	minimax: "MiniMax",
 	unbound: "Unbound",
 	requesty: "Requesty",
 	roo: "Roo",

+ 10 - 0
cli/src/constants/providers/models.ts

@@ -45,6 +45,8 @@ import {
 	claudeCodeDefaultModelId,
 	geminiCliModels,
 	geminiCliDefaultModelId,
+	minimaxModels,
+	minimaxDefaultModelId,
 } from "@roo-code/types"
 
 /**
@@ -117,6 +119,7 @@ export const PROVIDER_TO_ROUTER_NAME: Record<ProviderName, RouterName | null> =
 	moonshot: null,
 	deepseek: null,
 	doubao: null,
+	minimax: null,
 	"qwen-code": null,
 	"human-relay": null,
 	"fake-ai": null,
@@ -162,6 +165,7 @@ export const PROVIDER_MODEL_FIELD: Record<ProviderName, string | null> = {
 	moonshot: null,
 	deepseek: null,
 	doubao: null,
+	minimax: null,
 	"qwen-code": null,
 	"human-relay": null,
 	"fake-ai": null,
@@ -239,6 +243,7 @@ export const DEFAULT_MODEL_IDS: Partial<Record<ProviderName, string>> = {
 	sambanova: sambaNovaDefaultModelId,
 	featherless: featherlessDefaultModelId,
 	deepinfra: "deepseek-ai/DeepSeek-R1-0528",
+	minimax: "MiniMax-M2",
 	zai: internationalZAiDefaultModelId,
 	roo: rooDefaultModelId,
 	"gemini-cli": geminiCliDefaultModelId,
@@ -302,6 +307,11 @@ export function getModelsByProvider(params: {
 				models: moonshotModels as ModelRecord,
 				defaultModel: moonshotDefaultModelId,
 			}
+		case "minimax":
+			return {
+				models: minimaxModels as ModelRecord,
+				defaultModel: minimaxDefaultModelId,
+			}
 		case "deepseek":
 			return {
 				models: deepSeekModels as ModelRecord,

+ 18 - 1
cli/src/constants/providers/settings.ts

@@ -410,6 +410,18 @@ export const FIELD_REGISTRY: Record<string, FieldMetadata> = {
 		placeholder: "Enter API line...",
 	},
 
+	// Minimax fields
+	minimaxBaseUrl: {
+		label: "Base URL",
+		type: "text",
+		placeholder: "Enter MiniMax base URL...",
+	},
+	minimaxApiKey: {
+		label: "API Key",
+		type: "password",
+		placeholder: "Enter MiniMax API key...",
+	},
+
 	// Unbound fields
 	unboundApiKey: {
 		label: "API Key",
@@ -767,7 +779,11 @@ export const getProviderSettings = (provider: ProviderName, config: ProviderSett
 					type: "text",
 				},
 			]
-
+		case "minimax":
+			return [
+				createFieldConfig("minimaxBaseUrl", config, "https://api.minimax.io/anthropic"),
+				createFieldConfig("minimaxApiKey", config),
+			]
 		case "fake-ai":
 			return [
 				{
@@ -825,6 +841,7 @@ export const PROVIDER_DEFAULT_MODELS: Record<ProviderName, string> = {
 	"vercel-ai-gateway": "gpt-4o",
 	"virtual-quota-fallback": "gpt-4o",
 	"human-relay": "human",
+	minimax: "MiniMax-M2",
 	"fake-ai": "fake-model",
 }
 

+ 1 - 0
cli/src/constants/providers/validation.ts

@@ -44,4 +44,5 @@ export const PROVIDER_REQUIRED_FIELDS: Record<ProviderName, string[]> = {
 	vertex: [], // Has special validation logic (either/or fields)
 	"vscode-lm": [], // Has nested object validation
 	"virtual-quota-fallback": [], // Has array validation
+	minimax: ["minimaxBaseUrl", "minimaxApiKey", "apiModelId"],
 }

+ 51 - 1
cli/src/host/ExtensionHost.ts

@@ -25,6 +25,9 @@ export class ExtensionHost extends EventEmitter {
 	private extensionAPI: any = null
 	private vscodeAPI: any = null
 	private webviewProviders: Map<string, any> = new Map()
+	private webviewInitialized = false
+	private pendingMessages: WebviewMessage[] = []
+	private isInitialSetup = true
 	private originalConsole: {
 		log: typeof console.log
 		error: typeof console.error
@@ -273,6 +276,13 @@ export class ExtensionHost extends EventEmitter {
 				return
 			}
 
+			// Queue messages if webview not initialized
+			if (!this.webviewInitialized) {
+				this.pendingMessages.push(message)
+				logs.debug(`Queued message ${message.type} - webview not ready`, "ExtensionHost")
+				return
+			}
+
 			// Track extension message sent
 			getTelemetryService().trackExtensionMessageSent(message.type)
 
@@ -888,13 +898,53 @@ export class ExtensionHost extends EventEmitter {
 	// Methods for webview provider registration (called from VSCode API mock)
 	registerWebviewProvider(viewId: string, provider: any): void {
 		this.webviewProviders.set(viewId, provider)
-		logs.debug(`Registered webview provider: ${viewId}`, "ExtensionHost")
+		logs.info(`Webview provider registered: ${viewId}`, "ExtensionHost")
 	}
 
 	unregisterWebviewProvider(viewId: string): void {
 		this.webviewProviders.delete(viewId)
 		logs.debug(`Unregistered webview provider: ${viewId}`, "ExtensionHost")
 	}
+
+	/**
+	 * Mark webview as ready and flush pending messages
+	 * Called by VSCode mock after resolveWebviewView completes
+	 */
+	public markWebviewReady(): void {
+		this.webviewInitialized = true
+		this.isInitialSetup = false
+		logs.info("Webview marked as ready, flushing pending messages", "ExtensionHost")
+		this.flushPendingMessages()
+	}
+
+	/**
+	 * Flush all pending messages that were queued before webview was ready
+	 */
+	private flushPendingMessages(): void {
+		const messages = [...this.pendingMessages]
+		this.pendingMessages = []
+
+		logs.info(`Flushing ${messages.length} pending messages`, "ExtensionHost")
+		for (const message of messages) {
+			logs.debug(`Flushing pending message: ${message.type}`, "ExtensionHost")
+			// Use void to explicitly ignore the promise
+			void this.sendWebviewMessage(message)
+		}
+	}
+
+	/**
+	 * Check if webview is ready to receive messages
+	 */
+	public isWebviewReady(): boolean {
+		return this.webviewInitialized
+	}
+
+	/**
+	 * Check if this is the initial setup phase
+	 */
+	public isInInitialSetup(): boolean {
+		return this.isInitialSetup
+	}
 }
 
 export function createExtensionHost(options: ExtensionHostOptions): ExtensionHost {

+ 30 - 6
cli/src/host/VSCode.ts

@@ -1082,7 +1082,13 @@ export class WorkspaceAPI {
 		// In CLI mode, we need to apply the edits to the actual files
 		try {
 			for (const [uri, edits] of edit.entries()) {
-				const filePath = uri.fsPath
+				let filePath = uri.fsPath
+
+				// On Windows, strip leading slash if present (e.g., /C:/path becomes C:/path)
+				if (process.platform === "win32" && filePath.startsWith("/")) {
+					filePath = filePath.slice(1)
+				}
+
 				let content = ""
 
 				// Read existing content if file exists
@@ -1598,7 +1604,8 @@ export class WindowAPI {
 	registerWebviewViewProvider(viewId: string, provider: any, _options?: any): Disposable {
 		// Store the provider for later use by ExtensionHost
 		if ((global as any).__extensionHost) {
-			;(global as any).__extensionHost.registerWebviewProvider(viewId, provider)
+			const extensionHost = (global as any).__extensionHost
+			extensionHost.registerWebviewProvider(viewId, provider)
 
 			// Set up webview mock that captures messages from the extension
 			const mockWebview = {
@@ -1640,14 +1647,31 @@ export class WindowAPI {
 					visible: true,
 				}
 
-				// Call the provider's resolveWebviewView method
-				setTimeout(() => {
+				// Call resolveWebviewView immediately with initialization context
+				// No setTimeout needed - use event-based synchronization instead
+				;(async () => {
 					try {
-						provider.resolveWebviewView(mockWebviewView, { preserveFocus: false }, {})
+						// Pass isInitialSetup flag in context to prevent task abortion
+						const context = {
+							preserveFocus: false,
+							isInitialSetup: extensionHost.isInInitialSetup(),
+						}
+
+						logs.debug(
+							`Calling resolveWebviewView with isInitialSetup=${context.isInitialSetup}`,
+							"VSCode.Window",
+						)
+
+						// Await the result to ensure webview is fully initialized before marking ready
+						await provider.resolveWebviewView(mockWebviewView, context, {})
+
+						// Mark webview as ready after resolution completes
+						extensionHost.markWebviewReady()
+						logs.debug("Webview resolution complete, marked as ready", "VSCode.Window")
 					} catch (error) {
 						logs.error("Error resolving webview view", "VSCode.Window", { error })
 					}
-				}, 100)
+				})()
 			}
 		}
 		return {

+ 296 - 0
cli/src/host/__tests__/ExtensionHost.raceCondition.test.ts

@@ -0,0 +1,296 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
+import { ExtensionHost } from "../ExtensionHost.js"
+import type { WebviewMessage } from "../../types/messages.js"
+
+describe("ExtensionHost Race Condition Fix", () => {
+	let extensionHost: ExtensionHost
+
+	beforeEach(() => {
+		extensionHost = new ExtensionHost({
+			workspacePath: "/test/workspace",
+			extensionBundlePath: "/test/extension.js",
+			extensionRootPath: "/test",
+		})
+
+		// Initialize state and mark as activated for testing
+		const initializeState = (extensionHost as any).initializeState.bind(extensionHost)
+		initializeState()
+		;(extensionHost as any).isActivated = true
+	})
+
+	afterEach(() => {
+		vi.clearAllMocks()
+	})
+
+	describe("Message Queuing", () => {
+		it("should queue messages when webview is not initialized", async () => {
+			const message: WebviewMessage = {
+				type: "newTask",
+				text: "Test task",
+			}
+
+			// Webview should not be ready initially
+			expect(extensionHost.isWebviewReady()).toBe(false)
+
+			// Send message - it should be queued
+			await extensionHost.sendWebviewMessage(message)
+
+			// Verify message was queued (check internal state)
+			const pendingMessages = (extensionHost as any).pendingMessages
+			expect(pendingMessages).toHaveLength(1)
+			expect(pendingMessages[0]).toEqual(message)
+		})
+
+		it("should queue multiple messages before webview is ready", async () => {
+			const messages: WebviewMessage[] = [
+				{ type: "mode", text: "code" },
+				{ type: "newTask", text: "Test task" },
+				{ type: "telemetrySetting", text: "enabled" },
+			]
+
+			// Send all messages
+			for (const message of messages) {
+				await extensionHost.sendWebviewMessage(message)
+			}
+
+			// Verify all messages were queued
+			const pendingMessages = (extensionHost as any).pendingMessages
+			expect(pendingMessages).toHaveLength(3)
+			expect(pendingMessages).toEqual(messages)
+		})
+
+		it("should not queue messages after webview is ready", async () => {
+			// Mark webview as ready
+			extensionHost.markWebviewReady()
+
+			const message: WebviewMessage = {
+				type: "newTask",
+				text: "Test task",
+			}
+
+			// Mock the webview provider to prevent actual message sending
+			const mockProvider = {
+				handleCLIMessage: vi.fn(),
+			}
+			;(extensionHost as any).webviewProviders.set("kilo-code.SidebarProvider", mockProvider)
+
+			// Send message - it should not be queued
+			await extensionHost.sendWebviewMessage(message)
+
+			// Verify message was not queued
+			const pendingMessages = (extensionHost as any).pendingMessages
+			expect(pendingMessages).toHaveLength(0)
+
+			// Verify message was sent directly
+			expect(mockProvider.handleCLIMessage).toHaveBeenCalledWith(message)
+		})
+	})
+
+	describe("Message Flushing", () => {
+		it("should flush all pending messages when webview becomes ready", async () => {
+			const messages: WebviewMessage[] = [
+				{ type: "mode", text: "code" },
+				{ type: "newTask", text: "Test task" },
+			]
+
+			// Queue messages
+			for (const message of messages) {
+				await extensionHost.sendWebviewMessage(message)
+			}
+
+			// Verify messages are queued
+			expect((extensionHost as any).pendingMessages).toHaveLength(2)
+
+			// Mock the webview provider
+			const mockProvider = {
+				handleCLIMessage: vi.fn(),
+			}
+			;(extensionHost as any).webviewProviders.set("kilo-code.SidebarProvider", mockProvider)
+
+			// Mark webview as ready - this should flush messages
+			extensionHost.markWebviewReady()
+
+			// Wait for async flush to complete
+			await new Promise((resolve) => setTimeout(resolve, 10))
+
+			// Verify pending messages were cleared
+			expect((extensionHost as any).pendingMessages).toHaveLength(0)
+
+			// Verify all messages were sent
+			expect(mockProvider.handleCLIMessage).toHaveBeenCalledTimes(2)
+		})
+
+		it("should maintain message order when flushing", async () => {
+			const messages: WebviewMessage[] = [
+				{ type: "mode", text: "code" },
+				{ type: "telemetrySetting", text: "enabled" },
+				{ type: "newTask", text: "Test task" },
+			]
+
+			// Queue messages
+			for (const message of messages) {
+				await extensionHost.sendWebviewMessage(message)
+			}
+
+			// Mock the webview provider to track call order
+			const callOrder: string[] = []
+			const mockProvider = {
+				handleCLIMessage: vi.fn((msg: WebviewMessage) => {
+					callOrder.push(msg.type)
+				}),
+			}
+			;(extensionHost as any).webviewProviders.set("kilo-code.SidebarProvider", mockProvider)
+
+			// Mark webview as ready
+			extensionHost.markWebviewReady()
+
+			// Wait for async flush
+			await new Promise((resolve) => setTimeout(resolve, 10))
+
+			// Verify messages were sent in order
+			expect(callOrder).toEqual(["mode", "telemetrySetting", "newTask"])
+		})
+	})
+
+	describe("Initialization State", () => {
+		it("should start in initial setup mode", () => {
+			expect(extensionHost.isInInitialSetup()).toBe(true)
+		})
+
+		it("should exit initial setup mode when webview is marked ready", () => {
+			expect(extensionHost.isInInitialSetup()).toBe(true)
+
+			extensionHost.markWebviewReady()
+
+			expect(extensionHost.isInInitialSetup()).toBe(false)
+		})
+
+		it("should not be ready initially", () => {
+			expect(extensionHost.isWebviewReady()).toBe(false)
+		})
+
+		it("should be ready after markWebviewReady is called", () => {
+			extensionHost.markWebviewReady()
+
+			expect(extensionHost.isWebviewReady()).toBe(true)
+		})
+	})
+
+	describe("Race Condition Prevention", () => {
+		it("should prevent task abortion by queuing newTask messages", async () => {
+			// Simulate the race condition scenario:
+			// 1. Extension activates
+			// 2. CLI sends newTask immediately
+			// 3. Webview resolves later
+
+			const newTaskMessage: WebviewMessage = {
+				type: "newTask",
+				text: "Test task from CLI",
+			}
+
+			// Send newTask before webview is ready (simulating CLI auto mode)
+			await extensionHost.sendWebviewMessage(newTaskMessage)
+
+			// Verify message is queued, not processed
+			const pendingMessages = (extensionHost as any).pendingMessages
+			expect(pendingMessages).toHaveLength(1)
+			expect(pendingMessages[0].type).toBe("newTask")
+
+			// Mock provider
+			const mockProvider = {
+				handleCLIMessage: vi.fn(),
+			}
+			;(extensionHost as any).webviewProviders.set("kilo-code.SidebarProvider", mockProvider)
+
+			// Now mark webview as ready (simulating resolveWebviewView completion)
+			extensionHost.markWebviewReady()
+
+			// Wait for flush
+			await new Promise((resolve) => setTimeout(resolve, 10))
+
+			// Verify the newTask message was sent AFTER webview was ready
+			expect(mockProvider.handleCLIMessage).toHaveBeenCalledWith(newTaskMessage)
+			expect((extensionHost as any).pendingMessages).toHaveLength(0)
+		})
+
+		it("should handle configuration injection before task creation", async () => {
+			const configMessage: WebviewMessage = {
+				type: "mode",
+				text: "architect",
+			}
+
+			const taskMessage: WebviewMessage = {
+				type: "newTask",
+				text: "Design a system",
+			}
+
+			// Send config and task messages before webview is ready
+			await extensionHost.sendWebviewMessage(configMessage)
+			await extensionHost.sendWebviewMessage(taskMessage)
+
+			// Both should be queued
+			const pendingMessages = (extensionHost as any).pendingMessages
+			expect(pendingMessages).toHaveLength(2)
+
+			// Mock provider
+			const callOrder: string[] = []
+			const mockProvider = {
+				handleCLIMessage: vi.fn((msg: WebviewMessage) => {
+					callOrder.push(msg.type)
+				}),
+			}
+			;(extensionHost as any).webviewProviders.set("kilo-code.SidebarProvider", mockProvider)
+
+			// Mark ready
+			extensionHost.markWebviewReady()
+			await new Promise((resolve) => setTimeout(resolve, 10))
+
+			// Verify config was sent before task
+			expect(callOrder).toEqual(["mode", "newTask"])
+		})
+	})
+
+	describe("Edge Cases", () => {
+		it("should handle markWebviewReady being called multiple times", () => {
+			extensionHost.markWebviewReady()
+			expect(extensionHost.isWebviewReady()).toBe(true)
+
+			// Call again - should not cause issues
+			extensionHost.markWebviewReady()
+			expect(extensionHost.isWebviewReady()).toBe(true)
+		})
+
+		it("should handle empty pending messages queue", () => {
+			// Mark ready with no pending messages
+			expect((extensionHost as any).pendingMessages).toHaveLength(0)
+
+			// Should not throw
+			expect(() => extensionHost.markWebviewReady()).not.toThrow()
+
+			expect(extensionHost.isWebviewReady()).toBe(true)
+		})
+
+		it("should handle messages sent during flush", async () => {
+			// Queue initial message
+			await extensionHost.sendWebviewMessage({ type: "mode", text: "code" })
+
+			// Mock provider that sends another message during handling
+			const mockProvider = {
+				handleCLIMessage: vi.fn(async (msg: WebviewMessage) => {
+					if (msg.type === "mode") {
+						// Send another message during flush
+						await extensionHost.sendWebviewMessage({ type: "telemetrySetting", text: "enabled" })
+					}
+				}),
+			}
+			;(extensionHost as any).webviewProviders.set("kilo-code.SidebarProvider", mockProvider)
+
+			// Mark ready and flush
+			extensionHost.markWebviewReady()
+			await new Promise((resolve) => setTimeout(resolve, 20))
+
+			// Both messages should have been processed
+			expect(mockProvider.handleCLIMessage).toHaveBeenCalledTimes(2)
+		})
+	})
+})

+ 177 - 0
cli/src/host/__tests__/webview-async-resolution.test.ts

@@ -0,0 +1,177 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
+import { ExtensionHost } from "../ExtensionHost.js"
+import * as path from "path"
+import * as fs from "fs"
+import { fileURLToPath } from "url"
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = path.dirname(__filename)
+
+describe("Webview Async Resolution", () => {
+	let extensionHost: ExtensionHost
+	let tempDir: string
+
+	beforeEach(() => {
+		// Create a temporary directory for the test
+		tempDir = path.join(__dirname, "..", "..", "..", "test-temp", `test-${Date.now()}`)
+		fs.mkdirSync(tempDir, { recursive: true })
+
+		// Create a mock extension bundle
+		const mockExtensionPath = path.join(tempDir, "extension.js")
+		fs.writeFileSync(
+			mockExtensionPath,
+			`
+			module.exports = {
+				activate: function(context) {
+					return {
+						getState: () => null,
+						sendMessage: () => {},
+					}
+				},
+				deactivate: function() {}
+			}
+		`,
+		)
+
+		extensionHost = new ExtensionHost({
+			workspacePath: tempDir,
+			extensionBundlePath: mockExtensionPath,
+			extensionRootPath: tempDir,
+		})
+	})
+
+	afterEach(async () => {
+		await extensionHost.deactivate()
+		// Clean up temp directory
+		if (fs.existsSync(tempDir)) {
+			fs.rmSync(tempDir, { recursive: true, force: true })
+		}
+	})
+
+	it("should await async resolveWebviewView before marking ready", async () => {
+		const resolutionOrder: string[] = []
+		let resolvePromise: () => void
+
+		// Create a promise that we control
+		const asyncResolution = new Promise<void>((resolve) => {
+			resolvePromise = resolve
+		})
+
+		// Mock provider with async resolveWebviewView
+		const mockProvider = {
+			resolveWebviewView: vi.fn(async () => {
+				resolutionOrder.push("resolveWebviewView-start")
+				await asyncResolution
+				resolutionOrder.push("resolveWebviewView-end")
+			}),
+		}
+
+		// Activate extension
+		await extensionHost.activate()
+
+		// Register the provider (simulating what VSCode API does)
+		extensionHost.registerWebviewProvider("test-provider", mockProvider)
+
+		// Simulate the webview registration flow
+		const vscode = (global as any).vscode
+		if (vscode && vscode.window) {
+			// This will trigger resolveWebviewView
+			vscode.window.registerWebviewViewProvider("test-provider", mockProvider)
+		}
+
+		// Wait a bit to ensure resolveWebviewView is called
+		await new Promise((resolve) => setTimeout(resolve, 50))
+
+		// At this point, resolveWebviewView should be running but not complete
+		expect(resolutionOrder).toContain("resolveWebviewView-start")
+		expect(resolutionOrder).not.toContain("resolveWebviewView-end")
+
+		// Webview should NOT be ready yet
+		expect(extensionHost.isWebviewReady()).toBe(false)
+
+		// Now complete the async resolution
+		resolvePromise!()
+
+		// Wait for the resolution to complete
+		await new Promise((resolve) => setTimeout(resolve, 50))
+
+		// Now it should be complete
+		expect(resolutionOrder).toContain("resolveWebviewView-end")
+
+		// And webview should be ready
+		expect(extensionHost.isWebviewReady()).toBe(true)
+	})
+
+	it("should handle synchronous resolveWebviewView correctly", async () => {
+		const mockProvider = {
+			resolveWebviewView: vi.fn(() => {
+				// Synchronous - no promise returned
+			}),
+		}
+
+		// Activate extension
+		await extensionHost.activate()
+
+		// Register the provider
+		extensionHost.registerWebviewProvider("test-provider", mockProvider)
+
+		// Simulate the webview registration flow
+		const vscode = (global as any).vscode
+		if (vscode && vscode.window) {
+			vscode.window.registerWebviewViewProvider("test-provider", mockProvider)
+		}
+
+		// Wait for registration to complete
+		await new Promise((resolve) => setTimeout(resolve, 50))
+
+		// Webview should be ready
+		expect(extensionHost.isWebviewReady()).toBe(true)
+	})
+
+	it("should queue messages until async resolution completes", async () => {
+		let resolvePromise: () => void
+		const asyncResolution = new Promise<void>((resolve) => {
+			resolvePromise = resolve
+		})
+
+		const receivedMessages: any[] = []
+		const mockProvider = {
+			resolveWebviewView: vi.fn(async () => {
+				await asyncResolution
+			}),
+			handleCLIMessage: vi.fn((message: any) => {
+				receivedMessages.push(message)
+			}),
+		}
+
+		// Activate extension
+		await extensionHost.activate()
+
+		// Register the provider
+		extensionHost.registerWebviewProvider("kilo-code.SidebarProvider", mockProvider)
+
+		// Simulate the webview registration flow
+		const vscode = (global as any).vscode
+		if (vscode && vscode.window) {
+			vscode.window.registerWebviewViewProvider("kilo-code.SidebarProvider", mockProvider)
+		}
+
+		// Send messages before resolution completes
+		await extensionHost.sendWebviewMessage({ type: "test1" })
+		await extensionHost.sendWebviewMessage({ type: "test2" })
+
+		// Messages should be queued, not received yet
+		expect(receivedMessages).toHaveLength(0)
+		expect(extensionHost.isWebviewReady()).toBe(false)
+
+		// Complete the async resolution
+		resolvePromise!()
+		await new Promise((resolve) => setTimeout(resolve, 100))
+
+		// Now webview should be ready and messages should be flushed
+		expect(extensionHost.isWebviewReady()).toBe(true)
+		expect(receivedMessages).toHaveLength(2)
+		expect(receivedMessages[0].type).toBe("test1")
+		expect(receivedMessages[1].type).toBe("test2")
+	})
+})

+ 14 - 0
cli/src/index.ts

@@ -29,6 +29,7 @@ program
 	.option("-w, --workspace <path>", "Path to the workspace directory", process.cwd())
 	.option("-a, --auto", "Run in autonomous mode (non-interactive)", false)
 	.option("-j, --json", "Output messages as JSON (requires --auto)", false)
+	.option("-c, --continue", "Resume the last conversation from this workspace", false)
 	.option("-t, --timeout <seconds>", "Timeout in seconds for autonomous mode (requires --auto)", parseInt)
 	.option(
 		"-p, --parallel",
@@ -98,6 +99,18 @@ program
 			process.exit(1)
 		}
 
+		// Validate that continue mode is not used with autonomous mode
+		if (options.continue && options.auto) {
+			console.error("Error: --continue option cannot be used with --auto flag")
+			process.exit(1)
+		}
+
+		// Validate that continue mode is not used with a prompt
+		if (options.continue && finalPrompt) {
+			console.error("Error: --continue option cannot be used with a prompt argument")
+			process.exit(1)
+		}
+
 		// Track autonomous mode start if applicable
 		if (options.auto && finalPrompt) {
 			getTelemetryService().trackCIModeStarted(finalPrompt.length, options.timeout)
@@ -139,6 +152,7 @@ program
 			timeout: options.timeout,
 			parallel: options.parallel,
 			worktreeBranch,
+			continue: options.continue,
 		})
 		await cli.start()
 		await cli.dispose()

+ 16 - 1
cli/src/services/__tests__/ExtensionService.test.ts

@@ -95,7 +95,16 @@ describe("ExtensionService", () => {
 
 		// Create a mock extension module
 		mockExtensionModule = {
-			activate: vi.fn(async () => {
+			activate: vi.fn(async (context) => {
+				// Register a mock webview provider immediately to prevent hanging
+				// This simulates the extension registering its provider during activation
+				if ((global as any).__extensionHost) {
+					const mockProvider = {
+						handleCLIMessage: vi.fn(async () => {}),
+					}
+					;(global as any).__extensionHost.registerWebviewProvider("kilo-code.SidebarProvider", mockProvider)
+				}
+
 				// Return a mock API
 				return {
 					getState: vi.fn(() => ({
@@ -256,6 +265,9 @@ describe("ExtensionService", () => {
 		it("should emit stateChange event when state changes", async () => {
 			await service.initialize()
 
+			// Mark webview as ready to allow messages to be processed
+			service.getExtensionHost().markWebviewReady()
+
 			const stateChangeHandler = vi.fn()
 			service.on("stateChange", stateChangeHandler)
 
@@ -275,6 +287,9 @@ describe("ExtensionService", () => {
 		it("should emit message event for extension messages", async () => {
 			await service.initialize()
 
+			// Mark webview as ready to allow messages to be processed
+			service.getExtensionHost().markWebviewReady()
+
 			const messageHandler = vi.fn()
 			service.on("message", messageHandler)
 

+ 1 - 0
cli/src/services/autocomplete.ts

@@ -481,6 +481,7 @@ function createProviderContext(
 			updateProviderModel: commandContext.updateProviderModel,
 			refreshRouterModels: commandContext.refreshRouterModels,
 			taskHistoryData: commandContext.taskHistoryData || null,
+			chatMessages: commandContext.chatMessages || [],
 		}
 	}
 

+ 9 - 2
cli/src/services/extension.ts

@@ -74,6 +74,7 @@ export class ExtensionService extends EventEmitter {
 	private options: Required<Omit<ExtensionServiceOptions, "identity">> & { identity?: IdentityInfo }
 	private isInitialized = false
 	private isDisposed = false
+	private isActivated = false
 
 	constructor(options: ExtensionServiceOptions = {}) {
 		super()
@@ -117,6 +118,7 @@ export class ExtensionService extends EventEmitter {
 		// Extension host events
 		this.extensionHost.on("activated", (api: ExtensionAPI) => {
 			logs.info("Extension host activated", "ExtensionService")
+			this.isActivated = true
 			this.emit("ready", api)
 		})
 
@@ -229,6 +231,10 @@ export class ExtensionService extends EventEmitter {
 			throw new Error("ExtensionService not initialized. Call initialize() first.")
 		}
 
+		if (!this.isActivated) {
+			throw new Error("ExtensionService not ready. Extension host not activated yet.")
+		}
+
 		if (this.isDisposed) {
 			throw new Error("Cannot send message on disposed ExtensionService")
 		}
@@ -301,10 +307,10 @@ export class ExtensionService extends EventEmitter {
 	}
 
 	/**
-	 * Check if the service is initialized
+	 * Check if the service is initialized and activated
 	 */
 	isReady(): boolean {
-		return this.isInitialized && !this.isDisposed
+		return this.isInitialized && this.isActivated && !this.isDisposed
 	}
 
 	/**
@@ -331,6 +337,7 @@ export class ExtensionService extends EventEmitter {
 
 			this.isDisposed = true
 			this.isInitialized = false
+			this.isActivated = false
 
 			// Emit disposed event
 			this.emit("disposed")

+ 673 - 0
cli/src/state/atoms/__tests__/shell.test.ts

@@ -0,0 +1,673 @@
+import { describe, it, expect, beforeEach, vi } from "vitest"
+import { createStore } from "jotai"
+import { shellModeActiveAtom, toggleShellModeAtom, executeShellCommandAtom, keyboardHandlerAtom } from "../keyboard.js"
+import { inputModeAtom } from "../ui.js"
+import type { Key } from "../../../types/keyboard.js"
+import {
+	shellHistoryAtom,
+	shellHistoryIndexAtom,
+	navigateShellHistoryUpAtom,
+	navigateShellHistoryDownAtom,
+	addToShellHistoryAtom,
+} from "../shell.js"
+import { textBufferStringAtom, setTextAtom } from "../textBuffer.js"
+
+// Mock child_process to avoid actual command execution
+vi.mock("child_process", () => ({
+	exec: vi.fn((command) => {
+		// Simulate successful command execution
+		const stdout = `Mock output for: ${command}`
+		const stderr = ""
+		const process = {
+			stdout: {
+				on: vi.fn((event, handler) => {
+					if (event === "data") {
+						setTimeout(() => handler(stdout), 10)
+					}
+				}),
+			},
+			stderr: {
+				on: vi.fn((event, handler) => {
+					if (event === "data") {
+						setTimeout(() => handler(stderr), 10)
+					}
+				}),
+			},
+			on: vi.fn((event, handler) => {
+				if (event === "close") {
+					setTimeout(() => handler(0), 20)
+				}
+			}),
+		}
+		return process
+	}),
+}))
+
+describe("shell mode - comprehensive tests", () => {
+	let store: ReturnType<typeof createStore>
+
+	beforeEach(() => {
+		store = createStore()
+		// Clear shell history before each test
+		store.set(shellHistoryAtom, [])
+		store.set(shellModeActiveAtom, false)
+		store.set(inputModeAtom, "normal" as const)
+		store.set(shellHistoryIndexAtom, -1)
+	})
+
+	describe("shell mode activation", () => {
+		it("should toggle shell mode on and off when input is empty", () => {
+			// Initial state
+			expect(store.get(shellModeActiveAtom)).toBe(false)
+			expect(store.get(inputModeAtom)).toBe("normal")
+			expect(store.get(textBufferStringAtom)).toBe("")
+
+			// Toggle on (input is empty)
+			store.set(toggleShellModeAtom)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+			expect(store.get(inputModeAtom)).toBe("shell")
+
+			// Toggle off
+			store.set(toggleShellModeAtom)
+			expect(store.get(shellModeActiveAtom)).toBe(false)
+			expect(store.get(inputModeAtom)).toBe("normal")
+		})
+
+		it("should NOT enter shell mode when input is not empty", () => {
+			// Set some text in the buffer
+			store.set(setTextAtom, "some text")
+			expect(store.get(textBufferStringAtom)).toBe("some text")
+
+			// Try to toggle on
+			store.set(toggleShellModeAtom)
+
+			// Should NOT activate shell mode
+			expect(store.get(shellModeActiveAtom)).toBe(false)
+			expect(store.get(inputModeAtom)).toBe("normal")
+
+			// Text should still be there
+			expect(store.get(textBufferStringAtom)).toBe("some text")
+		})
+
+		it("should exit shell mode even when text is present", () => {
+			// Enter shell mode (with empty input)
+			store.set(toggleShellModeAtom)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+
+			// Add some text
+			store.set(setTextAtom, "some command")
+
+			// Toggle off should work even with text
+			store.set(toggleShellModeAtom)
+			expect(store.get(shellModeActiveAtom)).toBe(false)
+			expect(store.get(inputModeAtom)).toBe("normal")
+		})
+
+		it("should reset history index when toggling on", () => {
+			// Set a non-default history index
+			store.set(shellHistoryIndexAtom, 5)
+
+			// Toggle on
+			store.set(toggleShellModeAtom)
+
+			// Index should be reset
+			expect(store.get(shellHistoryIndexAtom)).toBe(-1)
+		})
+
+		it("should reset history index when toggling off", () => {
+			// Activate shell mode and set history index
+			store.set(toggleShellModeAtom)
+			store.set(shellHistoryIndexAtom, 3)
+
+			// Toggle off
+			store.set(toggleShellModeAtom)
+
+			// Index should be reset
+			expect(store.get(shellHistoryIndexAtom)).toBe(-1)
+		})
+
+		it("should handle multiple rapid toggles when input is empty", () => {
+			// Toggle multiple times (with empty input)
+			store.set(toggleShellModeAtom)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+
+			store.set(toggleShellModeAtom)
+			expect(store.get(shellModeActiveAtom)).toBe(false)
+
+			store.set(toggleShellModeAtom)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+
+			store.set(toggleShellModeAtom)
+			expect(store.get(shellModeActiveAtom)).toBe(false)
+
+			// Final state should be consistent
+			expect(store.get(inputModeAtom)).toBe("normal")
+			expect(store.get(shellHistoryIndexAtom)).toBe(-1)
+		})
+	})
+
+	describe("shell command execution", () => {
+		it("should add commands to history", async () => {
+			const command = "echo 'test'"
+			await store.set(executeShellCommandAtom, command)
+
+			const history = store.get(shellHistoryAtom)
+			expect(history).toContain(command)
+			expect(history.length).toBe(1)
+		})
+
+		it("should not add empty commands to history", async () => {
+			const emptyCommand = "   "
+			await store.set(executeShellCommandAtom, emptyCommand)
+
+			const history = store.get(shellHistoryAtom)
+			expect(history).toHaveLength(0)
+		})
+
+		it("should trim whitespace from commands before adding to history", async () => {
+			const command = "  echo 'test'  "
+			await store.set(executeShellCommandAtom, command)
+
+			const history = store.get(shellHistoryAtom)
+			expect(history[0]).toBe("echo 'test'")
+		})
+
+		it("should add multiple unique commands to history", async () => {
+			await store.set(executeShellCommandAtom, "ls")
+			await store.set(executeShellCommandAtom, "pwd")
+			await store.set(executeShellCommandAtom, "echo test")
+
+			const history = store.get(shellHistoryAtom)
+			expect(history).toEqual(["ls", "pwd", "echo test"])
+		})
+
+		it("should reset history navigation index after command execution", async () => {
+			// Add a few commands to history
+			await store.set(executeShellCommandAtom, "echo 'test1'")
+			await store.set(executeShellCommandAtom, "echo 'test2'")
+
+			// Set history index to simulate navigation
+			store.set(shellHistoryIndexAtom, 1)
+			expect(store.get(shellHistoryIndexAtom)).toBe(1)
+
+			// Execute a new command
+			await store.set(executeShellCommandAtom, "echo 'test3'")
+
+			// History index should be reset to -1
+			expect(store.get(shellHistoryIndexAtom)).toBe(-1)
+		})
+
+		it("should allow duplicate commands in history", async () => {
+			await store.set(executeShellCommandAtom, "echo test")
+			await store.set(executeShellCommandAtom, "ls")
+			await store.set(executeShellCommandAtom, "echo test")
+
+			const history = store.get(shellHistoryAtom)
+			expect(history).toEqual(["echo test", "ls", "echo test"])
+		})
+	})
+
+	describe("shell history management", () => {
+		it("should limit history to 100 commands", async () => {
+			// Add 105 commands
+			for (let i = 0; i < 105; i++) {
+				await store.set(executeShellCommandAtom, `command${i}`)
+			}
+
+			const history = store.get(shellHistoryAtom)
+			expect(history).toHaveLength(100)
+			// Should keep the most recent 100
+			expect(history[0]).toBe("command5")
+			expect(history[99]).toBe("command104")
+		})
+
+		it("should add command to history with addToShellHistoryAtom", () => {
+			store.set(addToShellHistoryAtom, "test command")
+			const history = store.get(shellHistoryAtom)
+			expect(history).toContain("test command")
+		})
+
+		it("should maintain history order (newest last)", async () => {
+			await store.set(executeShellCommandAtom, "first")
+			await store.set(executeShellCommandAtom, "second")
+			await store.set(executeShellCommandAtom, "third")
+
+			const history = store.get(shellHistoryAtom)
+			expect(history).toEqual(["first", "second", "third"])
+		})
+	})
+
+	describe("history navigation - up", () => {
+		it("should navigate to most recent command on first up", () => {
+			// Add commands to history
+			store.set(shellHistoryAtom, ["cmd1", "cmd2", "cmd3"])
+
+			// Navigate up
+			store.set(navigateShellHistoryUpAtom)
+
+			expect(store.get(shellHistoryIndexAtom)).toBe(2)
+			expect(store.get(textBufferStringAtom)).toBe("cmd3")
+		})
+
+		it("should navigate to older commands with successive up presses", () => {
+			store.set(shellHistoryAtom, ["cmd1", "cmd2", "cmd3"])
+
+			// First up - most recent
+			store.set(navigateShellHistoryUpAtom)
+			expect(store.get(shellHistoryIndexAtom)).toBe(2)
+			expect(store.get(textBufferStringAtom)).toBe("cmd3")
+
+			// Second up - older
+			store.set(navigateShellHistoryUpAtom)
+			expect(store.get(shellHistoryIndexAtom)).toBe(1)
+			expect(store.get(textBufferStringAtom)).toBe("cmd2")
+
+			// Third up - oldest
+			store.set(navigateShellHistoryUpAtom)
+			expect(store.get(shellHistoryIndexAtom)).toBe(0)
+			expect(store.get(textBufferStringAtom)).toBe("cmd1")
+		})
+
+		it("should stop at oldest command", () => {
+			store.set(shellHistoryAtom, ["cmd1", "cmd2"])
+
+			// Navigate to oldest
+			store.set(navigateShellHistoryUpAtom)
+			store.set(navigateShellHistoryUpAtom)
+			expect(store.get(shellHistoryIndexAtom)).toBe(0)
+
+			// Try to go further up
+			store.set(navigateShellHistoryUpAtom)
+			expect(store.get(shellHistoryIndexAtom)).toBe(0)
+			expect(store.get(textBufferStringAtom)).toBe("cmd1")
+		})
+
+		it("should do nothing when history is empty", () => {
+			store.set(shellHistoryAtom, [])
+
+			store.set(navigateShellHistoryUpAtom)
+
+			expect(store.get(shellHistoryIndexAtom)).toBe(-1)
+			expect(store.get(textBufferStringAtom)).toBe("")
+		})
+
+		it("should handle single command history", () => {
+			store.set(shellHistoryAtom, ["only-cmd"])
+
+			store.set(navigateShellHistoryUpAtom)
+			expect(store.get(shellHistoryIndexAtom)).toBe(0)
+			expect(store.get(textBufferStringAtom)).toBe("only-cmd")
+
+			// Try to go up again
+			store.set(navigateShellHistoryUpAtom)
+			expect(store.get(shellHistoryIndexAtom)).toBe(0)
+		})
+	})
+
+	describe("history navigation - down", () => {
+		it("should do nothing when at default index", () => {
+			store.set(shellHistoryAtom, ["cmd1", "cmd2", "cmd3"])
+			expect(store.get(shellHistoryIndexAtom)).toBe(-1)
+
+			store.set(navigateShellHistoryDownAtom)
+
+			expect(store.get(shellHistoryIndexAtom)).toBe(-1)
+		})
+
+		it("should navigate to newer commands", () => {
+			store.set(shellHistoryAtom, ["cmd1", "cmd2", "cmd3", "cmd4"])
+
+			// Go to oldest
+			store.set(shellHistoryIndexAtom, 0)
+
+			// Navigate down to newer
+			store.set(navigateShellHistoryDownAtom)
+			expect(store.get(shellHistoryIndexAtom)).toBe(1)
+			expect(store.get(textBufferStringAtom)).toBe("cmd2")
+
+			store.set(navigateShellHistoryDownAtom)
+			expect(store.get(shellHistoryIndexAtom)).toBe(2)
+			expect(store.get(textBufferStringAtom)).toBe("cmd3")
+		})
+
+		it("should clear input when reaching most recent", () => {
+			store.set(shellHistoryAtom, ["cmd1", "cmd2"])
+
+			// Navigate up and then back down
+			store.set(navigateShellHistoryUpAtom) // index 1 (cmd2)
+			store.set(navigateShellHistoryUpAtom) // index 0 (cmd1)
+			store.set(navigateShellHistoryDownAtom) // index 1 (cmd2)
+			store.set(navigateShellHistoryDownAtom) // index -1 (clear)
+
+			expect(store.get(shellHistoryIndexAtom)).toBe(-1)
+			expect(store.get(textBufferStringAtom)).toBe("")
+		})
+
+		it("should handle navigation cycle: up then all the way down", () => {
+			store.set(shellHistoryAtom, ["cmd1", "cmd2", "cmd3"])
+
+			// Go up to recent
+			store.set(navigateShellHistoryUpAtom)
+			expect(store.get(textBufferStringAtom)).toBe("cmd3")
+
+			// Go all the way down to clear
+			store.set(navigateShellHistoryDownAtom)
+			expect(store.get(shellHistoryIndexAtom)).toBe(-1)
+			expect(store.get(textBufferStringAtom)).toBe("")
+		})
+	})
+
+	describe("history navigation - combined up/down", () => {
+		it("should handle mixed up/down navigation", () => {
+			store.set(shellHistoryAtom, ["cmd1", "cmd2", "cmd3", "cmd4"])
+
+			// Up twice
+			store.set(navigateShellHistoryUpAtom) // cmd4
+			store.set(navigateShellHistoryUpAtom) // cmd3
+			expect(store.get(textBufferStringAtom)).toBe("cmd3")
+
+			// Down once
+			store.set(navigateShellHistoryDownAtom) // cmd4
+			expect(store.get(textBufferStringAtom)).toBe("cmd4")
+
+			// Up once
+			store.set(navigateShellHistoryUpAtom) // cmd3
+			expect(store.get(textBufferStringAtom)).toBe("cmd3")
+
+			// Up to oldest
+			store.set(navigateShellHistoryUpAtom) // cmd2
+			store.set(navigateShellHistoryUpAtom) // cmd1
+			expect(store.get(textBufferStringAtom)).toBe("cmd1")
+		})
+	})
+
+	describe("Shift+1 key detection", () => {
+		it("should detect Shift+1 and toggle shell mode when input is empty", async () => {
+			const shift1Key: Key = {
+				name: "shift-1",
+				sequence: "!",
+				ctrl: false,
+				meta: false,
+				shift: true,
+				paste: false,
+			}
+
+			// Ensure input is empty
+			expect(store.get(textBufferStringAtom)).toBe("")
+
+			// Press Shift+1
+			await store.set(keyboardHandlerAtom, shift1Key)
+
+			// Should activate shell mode
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+			expect(store.get(inputModeAtom)).toBe("shell")
+		})
+
+		it("should toggle shell mode off with second Shift+1", async () => {
+			const shift1Key: Key = {
+				name: "shift-1",
+				sequence: "!",
+				ctrl: false,
+				meta: false,
+				shift: true,
+				paste: false,
+			}
+
+			// Activate (with empty input)
+			await store.set(keyboardHandlerAtom, shift1Key)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+
+			// Deactivate
+			await store.set(keyboardHandlerAtom, shift1Key)
+			expect(store.get(shellModeActiveAtom)).toBe(false)
+			expect(store.get(inputModeAtom)).toBe("normal")
+		})
+
+		it("should NOT activate shell mode via Shift+1 when input has text", async () => {
+			const shift1Key: Key = {
+				name: "shift-1",
+				sequence: "!",
+				ctrl: false,
+				meta: false,
+				shift: true,
+				paste: false,
+			}
+
+			// Add text to input
+			store.set(setTextAtom, "some command")
+			expect(store.get(textBufferStringAtom)).toBe("some command")
+
+			// Try to activate with Shift+1
+			await store.set(keyboardHandlerAtom, shift1Key)
+
+			// Should NOT activate shell mode
+			expect(store.get(shellModeActiveAtom)).toBe(false)
+			expect(store.get(inputModeAtom)).toBe("normal")
+		})
+
+		it("should insert '!' when Shift+1 is pressed with text in input", async () => {
+			const shift1Key: Key = {
+				name: "shift-1",
+				sequence: "!",
+				ctrl: false,
+				meta: false,
+				shift: true,
+				paste: false,
+			}
+
+			// Add text to input
+			store.set(setTextAtom, "some command")
+			expect(store.get(textBufferStringAtom)).toBe("some command")
+
+			// Press Shift+1
+			await store.set(keyboardHandlerAtom, shift1Key)
+
+			// Should NOT activate shell mode
+			expect(store.get(shellModeActiveAtom)).toBe(false)
+			expect(store.get(inputModeAtom)).toBe("normal")
+
+			// Should insert "!" into the text
+			expect(store.get(textBufferStringAtom)).toBe("some command!")
+		})
+	})
+
+	describe("@ tag (file mention) handling in shell mode", () => {
+		it("should not trigger file mention autocomplete when typing @ in shell mode", () => {
+			// Enter shell mode
+			store.set(toggleShellModeAtom)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+
+			// Type text with @ symbol
+			store.set(setTextAtom, "git commit -m 'fix @username'")
+
+			// Verify shell mode is still active
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+			expect(store.get(inputModeAtom)).toBe("shell")
+
+			// The @ should be treated as regular text, not triggering file mentions
+			expect(store.get(textBufferStringAtom)).toBe("git commit -m 'fix @username'")
+		})
+
+		it("should allow @ symbols in email addresses in shell mode", () => {
+			// Enter shell mode
+			store.set(toggleShellModeAtom)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+
+			// Type command with email
+			const emailCommand = "git config user.email [email protected]"
+			store.set(setTextAtom, emailCommand)
+
+			// Verify the @ is preserved as normal text
+			expect(store.get(textBufferStringAtom)).toBe(emailCommand)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+		})
+
+		it("should allow multiple @ symbols in shell commands", () => {
+			// Enter shell mode
+			store.set(toggleShellModeAtom)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+
+			// Type command with multiple @ symbols
+			const command = "echo 'user@host, admin@host, test@domain'"
+			store.set(setTextAtom, command)
+
+			// Verify all @ symbols are preserved
+			expect(store.get(textBufferStringAtom)).toBe(command)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+		})
+
+		it("should allow @ in shell command arguments", () => {
+			// Enter shell mode
+			store.set(toggleShellModeAtom)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+
+			// Type command with @ in various positions
+			const command = "ssh [email protected] -p 22"
+			store.set(setTextAtom, command)
+
+			// Verify @ is treated as normal text
+			expect(store.get(textBufferStringAtom)).toBe(command)
+		})
+
+		it("should handle @ at the start of a shell command", () => {
+			// Enter shell mode
+			store.set(toggleShellModeAtom)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+
+			// Type command starting with @
+			store.set(setTextAtom, "@echo test")
+
+			// Verify @ is preserved
+			expect(store.get(textBufferStringAtom)).toBe("@echo test")
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+		})
+	})
+
+	describe("/ command suggestions in shell mode", () => {
+		it("should not trigger command suggestions when typing / in shell mode", () => {
+			// Enter shell mode
+			store.set(toggleShellModeAtom)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+
+			// Type text with / for paths
+			store.set(setTextAtom, "cd /home/user/projects")
+
+			// Verify shell mode is still active
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+			expect(store.get(inputModeAtom)).toBe("shell")
+
+			// The / should be treated as regular text (path separator), not triggering commands
+			expect(store.get(textBufferStringAtom)).toBe("cd /home/user/projects")
+		})
+
+		it("should allow absolute paths with / in shell mode", () => {
+			// Enter shell mode
+			store.set(toggleShellModeAtom)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+
+			// Type command with absolute path
+			const pathCommand = "ls -la /var/log/"
+			store.set(setTextAtom, pathCommand)
+
+			// Verify the / is preserved as normal text
+			expect(store.get(textBufferStringAtom)).toBe(pathCommand)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+		})
+
+		it("should allow multiple slashes in file paths", () => {
+			// Enter shell mode
+			store.set(toggleShellModeAtom)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+
+			// Type command with multiple slashes
+			const command = "cat /etc/nginx/nginx.conf"
+			store.set(setTextAtom, command)
+
+			// Verify all / symbols are preserved
+			expect(store.get(textBufferStringAtom)).toBe(command)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+		})
+
+		it("should allow / at the start of a shell command (absolute paths)", () => {
+			// Enter shell mode
+			store.set(toggleShellModeAtom)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+
+			// Type command starting with / (absolute path)
+			store.set(setTextAtom, "/usr/bin/python3 script.py")
+
+			// Verify / is preserved
+			expect(store.get(textBufferStringAtom)).toBe("/usr/bin/python3 script.py")
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+		})
+
+		it("should allow URLs with / and @ in shell mode", () => {
+			// Enter shell mode
+			store.set(toggleShellModeAtom)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+
+			// Type command with URL containing both @ and /
+			const curlCommand = "curl https://[email protected]/api/endpoint"
+			store.set(setTextAtom, curlCommand)
+
+			// Verify both @ and / are preserved
+			expect(store.get(textBufferStringAtom)).toBe(curlCommand)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+		})
+
+		it("should allow git commands with / in branch names", () => {
+			// Enter shell mode
+			store.set(toggleShellModeAtom)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+
+			// Type git command with / in branch name
+			const gitCommand = "git checkout feature/add-new-feature"
+			store.set(setTextAtom, gitCommand)
+
+			// Verify / is preserved
+			expect(store.get(textBufferStringAtom)).toBe(gitCommand)
+			expect(store.get(shellModeActiveAtom)).toBe(true)
+		})
+	})
+
+	describe("edge cases", () => {
+		it("should handle empty string command gracefully", async () => {
+			await store.set(executeShellCommandAtom, "")
+			const history = store.get(shellHistoryAtom)
+			expect(history).toHaveLength(0)
+		})
+
+		it("should handle only whitespace command", async () => {
+			await store.set(executeShellCommandAtom, "   \t\n  ")
+			const history = store.get(shellHistoryAtom)
+			expect(history).toHaveLength(0)
+		})
+
+		it("should preserve history when toggling shell mode", () => {
+			store.set(shellHistoryAtom, ["cmd1", "cmd2"])
+
+			// Toggle on and off
+			store.set(toggleShellModeAtom)
+			store.set(toggleShellModeAtom)
+
+			// History should be preserved
+			const history = store.get(shellHistoryAtom)
+			expect(history).toEqual(["cmd1", "cmd2"])
+		})
+
+		it("should handle history navigation after clearing history", () => {
+			store.set(shellHistoryAtom, ["cmd1", "cmd2"])
+			store.set(navigateShellHistoryUpAtom)
+			expect(store.get(textBufferStringAtom)).toBe("cmd2")
+			const indexBeforeClear = store.get(shellHistoryIndexAtom)
+
+			// Clear history
+			store.set(shellHistoryAtom, [])
+
+			// Try to navigate - should return early and not change index
+			store.set(navigateShellHistoryUpAtom)
+			// Index should remain unchanged when history is empty
+			expect(store.get(shellHistoryIndexAtom)).toBe(indexBeforeClear)
+		})
+	})
+})

+ 5 - 2
cli/src/state/atoms/approval.ts

@@ -126,9 +126,12 @@ export const approvalOptionsAtom = atom<ApprovalOption[]>((get) => {
 
 	// Determine button labels based on ask type
 	let approveLabel = "Approve"
-	const rejectLabel = "Reject"
+	let rejectLabel = "Reject"
 
-	if (pendingMessage.ask === "tool") {
+	if (pendingMessage.ask === "checkpoint_restore") {
+		approveLabel = "Restore Checkpoint"
+		rejectLabel = "Cancel"
+	} else if (pendingMessage.ask === "tool") {
 		try {
 			const toolData = JSON.parse(pendingMessage.text || "{}")
 			const tool = toolData.tool

+ 6 - 1
cli/src/state/atoms/config.ts

@@ -8,6 +8,7 @@ import { addCustomTheme, removeCustomTheme, updateCustomTheme } from "../../cons
 import type { Theme } from "../../types/theme.js"
 import { logs } from "../../services/logs.js"
 import { getTelemetryService } from "../../services/telemetry/index.js"
+import { applyEnvOverrides } from "../../config/env-overrides.js"
 
 // Core config atom - holds the current configuration
 export const configAtom = atom<CLIConfig>(DEFAULT_CONFIG)
@@ -55,7 +56,11 @@ export const loadConfigAtom = atom(null, async (get, set, mode?: string) => {
 		set(configValidationAtom, result.validation)
 
 		// Override mode if provided (e.g., from CLI options)
-		const finalConfig = mode ? { ...result.config, mode } : result.config
+		let finalConfig = mode ? { ...result.config, mode } : result.config
+
+		// Apply environment variable overrides
+		finalConfig = applyEnvOverrides(finalConfig)
+
 		set(configAtom, finalConfig)
 
 		if (result.validation.valid) {

+ 12 - 0
cli/src/state/atoms/extension.ts

@@ -146,11 +146,23 @@ export const hasActiveTaskAtom = atom<boolean>((get) => {
 	return task !== null
 })
 
+/**
+ * Atom to track if the task was resumed via --continue flag
+ * Prevents showing "Task ready to resume" message when already resumed
+ */
+export const taskResumedViaContinueAtom = atom<boolean>(false)
+
 /**
  * Derived atom to check if there's a resume_task ask pending
  * This checks if the last message is a resume_task or resume_completed_task
+ * But doesn't show the message if the task was already resumed via --continue
  */
 export const hasResumeTaskAtom = atom<boolean>((get) => {
+	const taskResumedViaContinue = get(taskResumedViaContinueAtom)
+	if (taskResumedViaContinue) {
+		return false
+	}
+
 	const lastMessage = get(lastChatMessageAtom)
 	return lastMessage?.ask === "resume_task" || lastMessage?.ask === "resume_completed_task"
 })

+ 73 - 2
cli/src/state/atoms/keyboard.ts

@@ -51,6 +51,22 @@ import {
 	navigateHistoryUpAtom,
 	navigateHistoryDownAtom,
 } from "./history.js"
+import {
+	shellModeActiveAtom,
+	toggleShellModeAtom,
+	navigateShellHistoryUpAtom,
+	navigateShellHistoryDownAtom,
+	executeShellCommandAtom,
+} from "./shell.js"
+
+// Export shell atoms for backward compatibility
+export {
+	shellModeActiveAtom,
+	toggleShellModeAtom,
+	navigateShellHistoryUpAtom,
+	navigateShellHistoryDownAtom,
+	executeShellCommandAtom,
+}
 
 // ============================================================================
 // Core State Atoms
@@ -606,6 +622,46 @@ function handleHistoryKeys(get: any, set: any, key: Key): void {
 	}
 }
 
+/**
+ * Shell mode keyboard handler
+ * Handles shell command input and execution using existing text buffer
+ */
+async function handleShellKeys(get: any, set: any, key: Key): Promise<void> {
+	const currentInput = get(textBufferStringAtom)
+
+	switch (key.name) {
+		case "up": {
+			// Navigate shell history up
+			set(navigateShellHistoryUpAtom)
+			return
+		}
+
+		case "down": {
+			// Navigate shell history down
+			set(navigateShellHistoryDownAtom)
+			return
+		}
+
+		case "return":
+			if (!key.shift && !key.meta) {
+				// Execute shell command
+				set(executeShellCommandAtom, currentInput)
+				return
+			}
+			break
+
+		case "escape":
+			// Exit shell mode
+			set(toggleShellModeAtom)
+			return
+
+		default:
+			// Character input - let the default text input handlers deal with it
+			handleTextInputKeys(get, set, key)
+			return
+	}
+}
+
 /**
  * Unified text input keyboard handler
  * Handles both normal (single-line) and multiline text input
@@ -755,6 +811,17 @@ function handleGlobalHotkeys(get: any, set: any, key: Key): boolean {
 				return true
 			}
 			break
+		case "shift-1": {
+			// Toggle shell mode with Shift+1 or Shift+! only if input is empty
+			const isEmpty = get(textBufferIsEmptyAtom)
+			if (isEmpty) {
+				// Input is empty, toggle shell mode
+				set(toggleShellModeAtom)
+				return true
+			}
+			// Input has text, don't consume the key - let it be inserted as "!"
+			return false
+		}
 	}
 	return false
 }
@@ -775,15 +842,17 @@ export const keyboardHandlerAtom = atom(null, async (get, set, key: Key) => {
 	const isAutocompleteVisible = get(showAutocompleteAtom)
 	const fileMentionSuggestions = get(fileMentionSuggestionsAtom)
 	const isInHistoryMode = get(historyModeAtom)
+	const isShellModeActive = get(shellModeActiveAtom)
 
 	// Check if we have file mention suggestions (this means we're in file mention mode)
 	const hasFileMentions = fileMentionSuggestions.length > 0
 
-	// Mode priority: approval > followup > history > autocomplete (including file mentions) > normal
+	// Mode priority: shell > approval > followup > history > autocomplete (including file mentions) > normal
 	// History has higher priority than autocomplete because when navigating history,
 	// the text buffer may contain commands that start with "/" which would trigger autocomplete
 	let mode: InputMode = "normal"
-	if (isApprovalPending) mode = "approval"
+	if (isShellModeActive) mode = "shell"
+	else if (isApprovalPending) mode = "approval"
 	else if (isFollowupVisible) mode = "followup"
 	else if (isInHistoryMode) mode = "history"
 	else if (hasFileMentions || isAutocompleteVisible) mode = "autocomplete"
@@ -793,6 +862,8 @@ export const keyboardHandlerAtom = atom(null, async (get, set, key: Key) => {
 
 	// Route to appropriate handler
 	switch (mode) {
+		case "shell":
+			return await handleShellKeys(get, set, key)
 		case "approval":
 			return handleApprovalKeys(get, set, key)
 		case "followup":

+ 214 - 0
cli/src/state/atoms/shell.ts

@@ -0,0 +1,214 @@
+/**
+ * Jotai atoms for shell mode state management
+ */
+
+import { atom } from "jotai"
+import { addMessageAtom, inputModeAtom, type InputMode } from "./ui.js"
+import { exec } from "child_process"
+import { clearTextAtom, setTextAtom, textBufferIsEmptyAtom } from "./textBuffer.js"
+
+// ============================================================================
+// Workspace Path Atom
+// ============================================================================
+
+/**
+ * The workspace directory where shell commands should be executed
+ */
+export const workspacePathAtom = atom<string | null>(null)
+
+// ============================================================================
+// Shell Mode Atoms
+// ============================================================================
+
+/**
+ * Whether shell mode is currently active
+ */
+export const shellModeActiveAtom = atom<boolean>(false)
+
+/**
+ * Shell command history
+ */
+export const shellHistoryAtom = atom<string[]>([])
+
+/**
+ * Current shell history index (for navigation)
+ */
+export const shellHistoryIndexAtom = atom<number>(-1)
+
+/**
+ * Action atom to toggle shell mode
+ * Only enters shell mode if input is empty, but always allows exiting
+ */
+export const toggleShellModeAtom = atom(null, (get, set) => {
+	const isCurrentlyActive = get(shellModeActiveAtom)
+	const isEmpty = get(textBufferIsEmptyAtom)
+
+	if (!isCurrentlyActive) {
+		// Entering shell mode - only allow if input is empty
+		if (!isEmpty) {
+			// Don't enter shell mode if there's already text in the input
+			return
+		}
+		set(shellModeActiveAtom, true)
+		set(inputModeAtom, "shell" as InputMode)
+		set(shellHistoryIndexAtom, -1)
+		// Clear text buffer when entering shell mode
+		set(clearTextAtom)
+	} else {
+		// Exiting shell mode - always allow
+		set(shellModeActiveAtom, false)
+		set(inputModeAtom, "normal" as InputMode)
+		set(shellHistoryIndexAtom, -1)
+		// Clear text buffer when exiting shell mode
+		set(clearTextAtom)
+	}
+})
+
+/**
+ * Action atom to add command to shell history
+ */
+export const addToShellHistoryAtom = atom(null, (get, set, command: string) => {
+	const history = get(shellHistoryAtom)
+	const newHistory = [...history, command]
+	// Keep only last 100 commands
+	set(shellHistoryAtom, newHistory.slice(-100))
+})
+
+/**
+ * Action atom to navigate shell history up
+ */
+export const navigateShellHistoryUpAtom = atom(null, (get, set) => {
+	const history = get(shellHistoryAtom)
+	const currentIndex = get(shellHistoryIndexAtom)
+
+	if (history.length === 0) return
+
+	let newIndex: number
+	if (currentIndex === -1) {
+		// First time going up - go to most recent command
+		newIndex = history.length - 1
+	} else if (currentIndex > 0) {
+		// Go to older command
+		newIndex = currentIndex - 1
+	} else {
+		// Already at oldest command
+		return
+	}
+
+	set(shellHistoryIndexAtom, newIndex)
+
+	// Set the text buffer to the history command
+	set(setTextAtom, history[newIndex] || "")
+})
+
+/**
+ * Action atom to navigate shell history down
+ */
+export const navigateShellHistoryDownAtom = atom(null, (get, set) => {
+	const history = get(shellHistoryAtom)
+	const currentIndex = get(shellHistoryIndexAtom)
+
+	if (currentIndex === -1) return
+
+	let newIndex: number
+	if (currentIndex === history.length - 1) {
+		// At most recent command - clear input
+		newIndex = -1
+	} else {
+		// Go to newer command
+		newIndex = currentIndex + 1
+	}
+
+	set(shellHistoryIndexAtom, newIndex)
+
+	// Set the text buffer to the history command or clear it
+	if (newIndex === -1) {
+		set(clearTextAtom)
+	} else {
+		set(setTextAtom, history[newIndex] || "")
+	}
+})
+
+/**
+ * Action atom to execute shell command
+ */
+export const executeShellCommandAtom = atom(null, async (get, set, command: string) => {
+	if (!command.trim()) return
+
+	// Add to history
+	set(addToShellHistoryAtom, command.trim())
+
+	// Clear the text buffer immediately for better UX
+	set(clearTextAtom)
+
+	// Execute the command immediately (no approval needed)
+	try {
+		// Get the workspace path for command execution
+		const workspacePath = get(workspacePathAtom)
+		const executionDir = workspacePath || process.cwd()
+
+		// Execute command and capture output
+		const childProcess = exec(command, {
+			cwd: executionDir,
+			timeout: 30000, // 30 second timeout
+		})
+
+		let stdout = ""
+		let stderr = ""
+
+		// Collect output
+		childProcess.stdout?.on("data", (data) => {
+			stdout += data.toString()
+		})
+
+		childProcess.stderr?.on("data", (data) => {
+			stderr += data.toString()
+		})
+
+		// Wait for completion
+		await new Promise<void>((resolve, reject) => {
+			childProcess.on("close", (code) => {
+				if (code === 0) {
+					resolve()
+				} else {
+					reject(new Error(`Command exited with code ${code}`))
+				}
+			})
+
+			childProcess.on("error", (error) => {
+				reject(error)
+			})
+		})
+
+		const output = stdout || stderr || "Command executed successfully"
+
+		// Display as system message for visibility in CLI
+		const systemMessage = {
+			id: `shell-${Date.now()}`,
+			type: "system" as const,
+			ts: Date.now(),
+			content: `$ ${command}\n${output}`,
+			partial: false,
+		}
+
+		set(addMessageAtom, systemMessage)
+	} catch (error: unknown) {
+		// Handle errors and display them in the message system
+
+		const errorOutput = `❌ Error: ${error instanceof Error ? error.message : error}`
+
+		// Display as error message for visibility in CLI
+		const errorMessage = {
+			id: `shell-error-${Date.now()}`,
+			type: "error" as const,
+			ts: Date.now(),
+			content: `$ ${command}\n${errorOutput}`,
+			partial: false,
+		}
+
+		set(addMessageAtom, errorMessage)
+	}
+
+	// Reset history navigation index
+	set(shellHistoryIndexAtom, -1)
+})

+ 9 - 1
cli/src/state/atoms/ui.ts

@@ -141,6 +141,7 @@ export type InputMode =
 	| "autocomplete" // Command autocomplete active
 	| "followup" // Followup suggestions active
 	| "history" // History navigation mode
+	| "shell" // Shell mode for command execution
 
 /**
  * Current input mode
@@ -286,7 +287,14 @@ export const lastAskMessageAtom = atom<ExtensionChatMessage | null>((get) => {
 	const messages = get(chatMessagesAtom)
 
 	// Ask types that require user approval
-	const approvalAskTypes = ["tool", "command", "browser_action_launch", "use_mcp_server", "payment_required_prompt"]
+	const approvalAskTypes = [
+		"tool",
+		"command",
+		"browser_action_launch",
+		"use_mcp_server",
+		"payment_required_prompt",
+		"checkpoint_restore",
+	]
 
 	const lastMessage = messages[messages.length - 1]
 	if (

+ 61 - 25
cli/src/state/hooks/useApprovalHandler.ts

@@ -107,7 +107,7 @@ export function useApprovalHandler(): UseApprovalHandlerReturn {
 	const setRejectCallback = useSetAtom(rejectCallbackAtom)
 	const setExecuteSelectedCallback = useSetAtom(executeSelectedCallbackAtom)
 
-	const { sendAskResponse } = useWebviewMessage()
+	const { sendAskResponse, sendMessage } = useWebviewMessage()
 	const approvalTelemetry = useApprovalTelemetry()
 
 	const approve = useCallback(
@@ -154,24 +154,57 @@ export function useApprovalHandler(): UseApprovalHandlerReturn {
 				}
 				store.set(updateChatMessageByTsAtom, answeredMessage)
 
-				// Determine response type based on ask type
-				// payment_required_prompt needs special "retry_clicked" response
-				const responseType =
-					currentPendingApproval.ask === "payment_required_prompt" ? "retry_clicked" : "yesButtonClicked"
+				// Handle checkpoint restore separately - send direct webview message instead of ask response
+				if (currentPendingApproval.ask === "checkpoint_restore") {
+					try {
+						// Parse checkpoint data from the message text
+						const checkpointData = JSON.parse(currentPendingApproval.text || "{}")
+						const { commitHash, checkpointTs } = checkpointData
 
-				await sendAskResponse({
-					response: responseType,
-					...(text && { text }),
-					...(images && { images }),
-				})
+						logs.debug("Sending checkpoint restore message", "useApprovalHandler", {
+							commitHash,
+							checkpointTs,
+						})
 
-				logs.debug("Approval response sent successfully", "useApprovalHandler", {
-					ts: currentPendingApproval.ts,
-					responseType,
-				})
+						// Send direct webview message to trigger restore
+						await sendMessage({
+							type: "checkpointRestore",
+							payload: {
+								ts: checkpointTs,
+								commitHash: commitHash,
+								mode: "restore",
+							},
+						})
 
-				// Track manual approval
-				approvalTelemetry.trackManualApproval(currentPendingApproval)
+						logs.debug("Checkpoint restore message sent successfully", "useApprovalHandler", {
+							ts: currentPendingApproval.ts,
+						})
+					} catch (error) {
+						logs.error("Failed to parse checkpoint data or send restore message", "useApprovalHandler", {
+							error,
+						})
+						throw error
+					}
+				} else {
+					// Determine response type based on ask type
+					// payment_required_prompt needs special "retry_clicked" response
+					const responseType =
+						currentPendingApproval.ask === "payment_required_prompt" ? "retry_clicked" : "yesButtonClicked"
+
+					await sendAskResponse({
+						response: responseType,
+						...(text && { text }),
+						...(images && { images }),
+					})
+
+					logs.debug("Approval response sent successfully", "useApprovalHandler", {
+						ts: currentPendingApproval.ts,
+						responseType,
+					})
+
+					// Track manual approval
+					approvalTelemetry.trackManualApproval(currentPendingApproval)
+				}
 
 				// Complete processing atomically - this clears both pending and processing state
 				store.set(completeApprovalProcessingAtom)
@@ -182,7 +215,7 @@ export function useApprovalHandler(): UseApprovalHandlerReturn {
 				throw error
 			}
 		},
-		[store, sendAskResponse, approvalTelemetry],
+		[store, sendAskResponse, sendMessage, approvalTelemetry],
 	)
 
 	const reject = useCallback(
@@ -225,16 +258,19 @@ export function useApprovalHandler(): UseApprovalHandlerReturn {
 				}
 				store.set(updateChatMessageByTsAtom, answeredMessage)
 
-				await sendAskResponse({
-					response: "noButtonClicked",
-					...(text && { text }),
-					...(images && { images }),
-				})
+				// For checkpoint_restore, we don't need to send any message on cancel - just mark as answered
+				if (currentPendingApproval.ask !== "checkpoint_restore") {
+					await sendAskResponse({
+						response: "noButtonClicked",
+						...(text && { text }),
+						...(images && { images }),
+					})
 
-				logs.debug("Rejection response sent successfully", "useApprovalHandler")
+					logs.debug("Rejection response sent successfully", "useApprovalHandler")
 
-				// Track manual rejection
-				approvalTelemetry.trackManualRejection(currentPendingApproval)
+					// Track manual rejection
+					approvalTelemetry.trackManualRejection(currentPendingApproval)
+				}
 
 				// Complete processing atomically - this clears both pending and processing state
 				store.set(completeApprovalProcessingAtom)

+ 4 - 1
cli/src/state/hooks/useCommandContext.ts

@@ -16,7 +16,7 @@ import {
 	refreshTerminalAtom,
 } from "../atoms/ui.js"
 import { setModeAtom, setThemeAtom, providerAtom, updateProviderAtom, configAtom } from "../atoms/config.js"
-import { routerModelsAtom, extensionStateAtom, isParallelModeAtom } from "../atoms/extension.js"
+import { routerModelsAtom, extensionStateAtom, isParallelModeAtom, chatMessagesAtom } from "../atoms/extension.js"
 import { requestRouterModelsAtom } from "../atoms/actions.js"
 import { profileDataAtom, balanceDataAtom, profileLoadingAtom, balanceLoadingAtom } from "../atoms/profile.js"
 import {
@@ -88,6 +88,7 @@ export function useCommandContext(): UseCommandContextReturn {
 	const kilocodeDefaultModel = extensionState?.kilocodeDefaultModel || ""
 	const isParallelMode = useAtomValue(isParallelModeAtom)
 	const config = useAtomValue(configAtom)
+	const chatMessages = useAtomValue(chatMessagesAtom)
 
 	// Get profile state
 	const profileData = useAtomValue(profileDataAtom)
@@ -192,6 +193,7 @@ export function useCommandContext(): UseCommandContextReturn {
 				nextTaskHistoryPage,
 				previousTaskHistoryPage,
 				sendWebviewMessage: sendMessage,
+				chatMessages,
 			}
 		},
 		[
@@ -225,6 +227,7 @@ export function useCommandContext(): UseCommandContextReturn {
 			changeTaskHistoryPageAndFetch,
 			nextTaskHistoryPage,
 			previousTaskHistoryPage,
+			chatMessages,
 		],
 	)
 

+ 14 - 1
cli/src/state/hooks/useCommandInput.ts

@@ -42,6 +42,7 @@ import {
 	showAutocompleteMenuAtom,
 	getSelectedSuggestionAtom,
 } from "../atoms/ui.js"
+import { shellModeActiveAtom } from "../atoms/shell.js"
 import { textBufferStringAtom, textBufferCursorAtom } from "../atoms/textBuffer.js"
 import { routerModelsAtom, extensionStateAtom } from "../atoms/extension.js"
 import { providerAtom, updateProviderAtom } from "../atoms/config.js"
@@ -150,6 +151,7 @@ export function useCommandInput(): UseCommandInputReturn {
 	const inputValue = useAtomValue(textBufferStringAtom)
 	const cursor = useAtomValue(textBufferCursorAtom)
 	const showAutocomplete = useAtomValue(showAutocompleteAtom)
+	const isShellMode = useAtomValue(shellModeActiveAtom)
 	const commandSuggestions = useAtomValue(suggestionsAtom)
 	const argumentSuggestions = useAtomValue(argumentSuggestionsAtom)
 	const fileMentionSuggestions = useAtomValue(fileMentionSuggestionsAtom)
@@ -214,6 +216,17 @@ export function useCommandInput(): UseCommandInputReturn {
 	}, [showAutocompleteAction])
 
 	const updateSuggestions = useCallback(async () => {
+		// In shell mode, disable all autocomplete
+		// Shell commands use @ (emails, SSH), / (paths), and other special chars
+		// that shouldn't trigger autocomplete suggestions
+		if (isShellMode) {
+			// Clear all suggestion state
+			clearFileMentionAction()
+			setSuggestionsAction([])
+			setArgumentSuggestionsAction([])
+			return
+		}
+
 		// Calculate cursor position in the text (convert row/col to absolute position)
 		const lines = inputValue.split("\n")
 		let cursorPosition = 0
@@ -234,7 +247,6 @@ export function useCommandInput(): UseCommandInputReturn {
 			return
 		}
 
-		// Clear file mention state if not in context
 		clearFileMentionAction()
 
 		// Fall back to command/argument detection
@@ -286,6 +298,7 @@ export function useCommandInput(): UseCommandInputReturn {
 		inputValue,
 		cursor,
 		cwd,
+		isShellMode,
 		setSuggestionsAction,
 		setArgumentSuggestionsAction,
 		setFileMentionSuggestionsAction,

+ 14 - 1
cli/src/state/hooks/useHotkeys.ts

@@ -7,6 +7,7 @@ import { useMemo } from "react"
 import { isStreamingAtom, showFollowupSuggestionsAtom } from "../atoms/ui.js"
 import { useApprovalHandler } from "./useApprovalHandler.js"
 import { hasResumeTaskAtom } from "../atoms/extension.js"
+import { shellModeActiveAtom } from "../atoms/keyboard.js"
 
 export interface Hotkey {
 	/** The key combination (e.g., "Ctrl+C", "Cmd+X") */
@@ -47,6 +48,7 @@ export function useHotkeys(): UseHotkeysReturn {
 	const isStreaming = useAtomValue(isStreamingAtom)
 	const isFollowupVisible = useAtomValue(showFollowupSuggestionsAtom)
 	const hasResumeTask = useAtomValue(hasResumeTaskAtom)
+	const isShellModeActive = useAtomValue(shellModeActiveAtom)
 	const { isApprovalPending } = useApprovalHandler()
 
 	const modifierKey = useMemo(() => getModifierKey(), [])
@@ -80,12 +82,23 @@ export function useHotkeys(): UseHotkeysReturn {
 			]
 		}
 
+		// Priority 5: Shell mode hotkeys
+		if (isShellModeActive) {
+			return [
+				{ keys: "Up/Down", description: "history" },
+				{ keys: "Enter", description: "to execute" },
+				{ keys: "Esc", description: "to exit" },
+				{ keys: "!", description: "to exit shell mode" },
+			]
+		}
+
 		// Default: General command hints
 		return [
 			{ keys: "/help", description: "for commands" },
 			{ keys: "/mode", description: "to switch mode" },
+			{ keys: "!", description: "for shell mode", primary: true },
 		]
-	}, [hasResumeTask, isApprovalPending, isStreaming, isFollowupVisible, modifierKey])
+	}, [hasResumeTask, isApprovalPending, isStreaming, isFollowupVisible, isShellModeActive, modifierKey])
 
 	const shouldShow = hotkeys.length > 0
 

+ 5 - 0
cli/src/types/messages.ts

@@ -103,6 +103,7 @@ export type ProviderName =
 	| "io-intelligence"
 	| "roo"
 	| "vercel-ai-gateway"
+	| "minimax"
 
 // Provider Settings Entry for profile metadata
 export interface ProviderSettingsEntry {
@@ -320,6 +321,10 @@ export interface ProviderSettings {
 	vercelAiGatewayApiKey?: string
 	vercelAiGatewayModelId?: string
 
+	// MiniMax AI
+	minimaxBaseUrl?: "https://api.minimax.io/anthropic" | "https://api.minimaxi.com/anthropic"
+	minimaxApiKey?: string
+
 	// Allow additional fields for extensibility
 	[key: string]: any
 }

+ 9 - 0
cli/src/ui/UI.tsx

@@ -32,6 +32,7 @@ import { createConfigErrorInstructions, createWelcomeMessage } from "./utils/wel
 import { generateUpdateAvailableMessage, getAutoUpdateStatus } from "../utils/auto-update.js"
 import { generateNotificationMessage } from "../utils/notifications.js"
 import { notificationsAtom } from "../state/atoms/notifications.js"
+import { workspacePathAtom } from "../state/atoms/shell.js"
 import { useTerminal } from "../state/hooks/useTerminal.js"
 
 // Initialize commands on module load
@@ -58,6 +59,7 @@ export const UI: React.FC<UIAppProps> = ({ options, onExit }) => {
 	const resetHistoryNavigation = useSetAtom(resetHistoryNavigationAtom)
 	const exitHistoryMode = useSetAtom(exitHistoryModeAtom)
 	const setIsParallelMode = useSetAtom(isParallelModeAtom)
+	const setWorkspacePath = useSetAtom(workspacePathAtom)
 
 	// Use specialized hooks for command and message handling
 	const { executeCommand, isExecuting: isExecutingCommand } = useCommandHandler()
@@ -110,6 +112,13 @@ export const UI: React.FC<UIAppProps> = ({ options, onExit }) => {
 		}
 	}, [options.parallel, setIsParallelMode])
 
+	// Initialize workspace path for shell commands
+	useEffect(() => {
+		if (options.workspace) {
+			setWorkspacePath(options.workspace)
+		}
+	}, [options.workspace, setWorkspacePath])
+
 	// Handle CI mode exit
 	useEffect(() => {
 		if (shouldExit && options.ci) {

+ 76 - 41
cli/src/ui/components/CommandInput.tsx

@@ -3,11 +3,17 @@
  * Updated to use useCommandInput, useWebviewMessage, useApprovalHandler, and useFollowupSuggestions hooks
  */
 
-import React, { useEffect } from "react"
+import React, { useCallback, useEffect } from "react"
 import { Box, Text } from "ink"
 import { useSetAtom, useAtomValue, useAtom } from "jotai"
 import { submissionCallbackAtom } from "../../state/atoms/keyboard.js"
-import { selectedIndexAtom, isCommittingParallelModeAtom, commitCountdownSecondsAtom } from "../../state/atoms/ui.js"
+import {
+	selectedIndexAtom,
+	inputModeAtom,
+	isCommittingParallelModeAtom,
+	commitCountdownSecondsAtom,
+} from "../../state/atoms/ui.js"
+import { shellModeActiveAtom, executeShellCommandAtom } from "../../state/atoms/keyboard.js"
 import { MultilineTextInput } from "./MultilineTextInput.js"
 import { useCommandInput } from "../../state/hooks/useCommandInput.js"
 import { useApprovalHandler } from "../../state/hooks/useApprovalHandler.js"
@@ -32,6 +38,11 @@ export const CommandInput: React.FC<CommandInputProps> = ({
 	// Get theme colors
 	const theme = useTheme()
 
+	// Get shell mode state
+	const isShellModeActive = useAtomValue(shellModeActiveAtom)
+	const inputMode = useAtomValue(inputModeAtom)
+	const executeShellCommand = useSetAtom(executeShellCommandAtom)
+
 	// Use the command input hook for autocomplete functionality
 	const { isAutocompleteVisible, commandSuggestions, argumentSuggestions, fileMentionSuggestions } = useCommandInput()
 
@@ -71,11 +82,6 @@ export const CommandInput: React.FC<CommandInputProps> = ({
 		return () => clearInterval(interval)
 	}, [isCommittingParallelMode, setCountdownSeconds, resetCountdownSeconds])
 
-	// Set the submission callback so keyboard handler can trigger onSubmit
-	useEffect(() => {
-		setSubmissionCallback({ callback: onSubmit })
-	}, [onSubmit, setSubmissionCallback])
-
 	// Determine suggestion type for autocomplete menu
 	const suggestionType =
 		fileMentionSuggestions.length > 0
@@ -89,43 +95,72 @@ export const CommandInput: React.FC<CommandInputProps> = ({
 	// Determine if input should be disabled (during approval, when explicitly disabled, or when committing parallel mode)
 	const isInputDisabled = disabled || isApprovalPending || isCommittingParallelMode
 
+	// Enhanced submission handler for shell mode
+	const handleSubmit = useCallback(
+		(value: string) => {
+			if (isShellModeActive) {
+				// Execute as shell command
+				executeShellCommand(value)
+			} else {
+				// Normal submission
+				onSubmit(value)
+			}
+		},
+		[isShellModeActive, executeShellCommand, onSubmit],
+	)
+
+	// Set the submission callback so keyboard handler can trigger onSubmit
+	useEffect(() => {
+		setSubmissionCallback({ callback: handleSubmit })
+	}, [handleSubmit, setSubmissionCallback])
+
+	// Determine styling based on mode (priority: parallel mode > shell mode > approval > normal)
+	const isShellMode = inputMode === "shell"
+	const borderColor = isCommittingParallelMode
+		? theme.ui.border.active
+		: isShellMode
+			? theme.semantic.warning
+			: isApprovalPending
+				? theme.actions.pending
+				: theme.ui.border.active
+	const promptColor = isCommittingParallelMode
+		? theme.ui.border.active
+		: isShellMode
+			? theme.semantic.warning
+			: isApprovalPending
+				? theme.actions.pending
+				: theme.ui.border.active
+	const promptSymbol = isCommittingParallelMode ? "⏳ " : isShellMode ? "$ " : isApprovalPending ? "[!] " : "> "
+	const inputPlaceholder = isCommittingParallelMode
+		? `Committing your changes... (${countdownSeconds}s)`
+		: isShellMode
+			? "Type shell command..."
+			: isApprovalPending
+				? "Awaiting approval..."
+				: placeholder
+
 	return (
 		<Box flexDirection="column">
 			{/* Input field */}
-			<Box
-				borderStyle="round"
-				borderColor={
-					isCommittingParallelMode
-						? theme.ui.border.active
-						: isApprovalPending
-							? theme.actions.pending
-							: theme.ui.border.active
-				}
-				paddingX={1}>
-				<Text
-					color={
-						isCommittingParallelMode
-							? theme.ui.border.active
-							: isApprovalPending
-								? theme.actions.pending
-								: theme.ui.border.active
-					}
-					bold>
-					{isCommittingParallelMode ? "⏳ " : isApprovalPending ? "[!] " : "> "}
-				</Text>
-				<MultilineTextInput
-					placeholder={
-						isCommittingParallelMode
-							? `Committing your changes... (${countdownSeconds}s)`
-							: isApprovalPending
-								? "Awaiting approval..."
-								: placeholder
-					}
-					showCursor={!isInputDisabled}
-					maxLines={5}
-					width={Math.max(10, process.stdout.columns - 6)}
-					focus={!isInputDisabled}
-				/>
+			<Box borderStyle="round" borderColor={borderColor} paddingX={1}>
+				<Box flexDirection="row" alignItems="center">
+					<Text color={promptColor} bold>
+						{isShellMode && !isCommittingParallelMode && (
+							<>
+								<Text color={promptColor}>shell</Text>
+								<Text> </Text>
+							</>
+						)}
+						{promptSymbol}
+					</Text>
+					<MultilineTextInput
+						placeholder={inputPlaceholder}
+						showCursor={!isInputDisabled}
+						maxLines={5}
+						width={Math.max(10, isShellMode ? process.stdout.columns - 12 : process.stdout.columns - 6)}
+						focus={!isInputDisabled}
+					/>
+				</Box>
 			</Box>
 
 			{/* Approval menu - shown above input when approval is pending */}

+ 4 - 0
cli/src/ui/messages/extension/AskMessageRouter.tsx

@@ -17,6 +17,7 @@ import {
 	AskAutoApprovalMaxReachedMessage,
 	AskBrowserActionLaunchMessage,
 	AskResumeTaskMessage,
+	AskCheckpointRestoreMessage,
 } from "./ask/index.js"
 
 /**
@@ -84,6 +85,9 @@ export const AskMessageRouter: React.FC<MessageComponentProps> = ({ message }) =
 		case "resume_completed_task":
 			return <AskResumeTaskMessage message={message} />
 
+		case "checkpoint_restore":
+			return <AskCheckpointRestoreMessage message={message} />
+
 		default:
 			return <DefaultAskMessage message={message} />
 	}

+ 48 - 0
cli/src/ui/messages/extension/ask/AskCheckpointRestoreMessage.tsx

@@ -0,0 +1,48 @@
+import React from "react"
+import { Box, Text } from "ink"
+import type { MessageComponentProps } from "../types.js"
+import { getMessageIcon } from "../utils.js"
+import { useTheme } from "../../../../state/hooks/useTheme.js"
+import { logs } from "../../../../services/logs.js"
+
+/**
+ * Display checkpoint restore approval request
+ * Approval is handled centrally by useApprovalMonitor in UI.tsx
+ */
+export const AskCheckpointRestoreMessage: React.FC<MessageComponentProps> = ({ message }) => {
+	const theme = useTheme()
+	const icon = getMessageIcon("ask", "checkpoint_restore")
+
+	// Parse checkpoint data from message text
+	let confirmationText = message.text || "Restore checkpoint?"
+	try {
+		const data = JSON.parse(message.text || "{}")
+		if (data.confirmationText) {
+			confirmationText = data.confirmationText
+		}
+	} catch (e) {
+		logs.error("Failed to parse checkpoint_restore data", "AskCheckpointRestoreMessage", { error: e })
+	}
+
+	return (
+		<Box flexDirection="column" marginY={1}>
+			<Box>
+				<Text color={theme.semantic.warning} bold>
+					{icon} Checkpoint Restore
+				</Text>
+			</Box>
+
+			<Box marginLeft={2} marginTop={1}>
+				<Text color={theme.ui.text.primary}>{confirmationText}</Text>
+			</Box>
+
+			{message.isAnswered && (
+				<Box marginLeft={2} marginTop={1}>
+					<Text color={theme.ui.text.dimmed} dimColor>
+						✓ Answered
+					</Text>
+				</Box>
+			)}
+		</Box>
+	)
+}

+ 5 - 9
cli/src/ui/messages/extension/ask/AskUseMcpServerMessage.tsx

@@ -1,12 +1,7 @@
 import React from "react"
 import { Box, Text } from "ink"
 import type { MessageComponentProps } from "../types.js"
-import {
-	getMessageIcon,
-	parseMcpServerData,
-	formatContentWithMetadata,
-	buildMetadataString,
-} from "../utils.js"
+import { getMessageIcon, parseMcpServerData, formatContentWithMetadata, buildMetadataString } from "../utils.js"
 import { useTheme } from "../../../../state/hooks/useTheme.js"
 import { getBoxWidth } from "../../../utils/width.js"
 
@@ -38,7 +33,9 @@ export const AskUseMcpServerMessage: React.FC<MessageComponentProps> = ({ messag
 		const title = isToolUse ? "Use MCP Tool" : "Access MCP Resource"
 
 		// Format arguments if present
-		const formattedArgs = mcpData.arguments ? formatContentWithMetadata(mcpData.arguments, MAX_LINES, PREVIEW_LINES) : null
+		const formattedArgs = mcpData.arguments
+			? formatContentWithMetadata(mcpData.arguments, MAX_LINES, PREVIEW_LINES)
+			: null
 
 		return (
 			<Box flexDirection="column" marginY={1}>
@@ -81,8 +78,7 @@ export const AskUseMcpServerMessage: React.FC<MessageComponentProps> = ({ messag
 							borderStyle="single"
 							borderColor={theme.ui.border.default}
 							paddingX={1}
-							flexDirection="column"
-							>
+							flexDirection="column">
 							{formattedArgs.content.split("\n").map((line, index) => (
 								<Text key={index} color={theme.ui.text.dimmed} dimColor>
 									{line}

+ 1 - 0
cli/src/ui/messages/extension/ask/index.ts

@@ -10,3 +10,4 @@ export { AskReportBugMessage } from "./AskReportBugMessage.js"
 export { AskAutoApprovalMaxReachedMessage } from "./AskAutoApprovalMaxReachedMessage.js"
 export { AskBrowserActionLaunchMessage } from "./AskBrowserActionLaunchMessage.js"
 export { AskResumeTaskMessage } from "./AskResumeTaskMessage.js"
+export { AskCheckpointRestoreMessage } from "./AskCheckpointRestoreMessage.js"

+ 3 - 6
cli/src/ui/messages/extension/say/SayMcpServerResponseMessage.tsx

@@ -43,7 +43,7 @@ export const SayMcpServerResponseMessage: React.FC<MessageComponentProps> = ({ m
 		// Format content with metadata
 		const formatted = useMemo(
 			() => (responseText ? formatContentWithMetadata(responseText, MAX_LINES, PREVIEW_LINES) : null),
-			[responseText]
+			[responseText],
 		)
 
 		if (!responseText || !formatted) {
@@ -80,8 +80,7 @@ export const SayMcpServerResponseMessage: React.FC<MessageComponentProps> = ({ m
 					borderStyle="single"
 					borderColor={theme.ui.border.default}
 					paddingX={1}
-					flexDirection="column"
-					>
+					flexDirection="column">
 					{formatted.content.split("\n").map((line, index) => (
 						<Text key={index} color={formatted.isJson ? theme.ui.text.dimmed : theme.ui.text.primary}>
 							{line}
@@ -106,9 +105,7 @@ export const SayMcpServerResponseMessage: React.FC<MessageComponentProps> = ({ m
 					</Text>
 				</Box>
 				<Box marginLeft={2} marginTop={1}>
-					<Text color={theme.ui.text.dimmed}>
-						An error occurred while formatting the response.
-					</Text>
+					<Text color={theme.ui.text.dimmed}>An error occurred while formatting the response.</Text>
 				</Box>
 			</Box>
 		)

+ 1 - 1
cli/src/ui/messages/extension/utils.ts

@@ -273,7 +273,7 @@ export interface FormattedContent {
 export function formatContentWithMetadata(
 	text: string,
 	maxLines: number = 20,
-	previewLines: number = 5
+	previewLines: number = 5,
 ): FormattedContent {
 	if (!text) {
 		return {

+ 40 - 1
cli/src/ui/utils/keyParsing.ts

@@ -14,6 +14,8 @@ import {
 	MODIFIER_ALT_BIT,
 	MODIFIER_CTRL_BIT,
 	CHAR_CODE_ESC,
+	CHAR_CODE_EXCLAMATION,
+	CHAR_CODE_DIGIT_1,
 	KITTY_KEYCODE_TAB,
 	KITTY_KEYCODE_BACKSPACE,
 	KITTY_KEYCODE_ENTER,
@@ -236,6 +238,22 @@ export function parseKittySequence(buffer: string): ParseResult {
 				}
 			}
 
+			// Handle Shift+1/Shift+! for shell mode toggle
+			if (shift && (keyCode === CHAR_CODE_DIGIT_1 || keyCode === CHAR_CODE_EXCLAMATION)) {
+				return {
+					key: {
+						name: "shift-1",
+						ctrl,
+						meta: alt,
+						shift: true,
+						paste: false,
+						sequence: buffer.slice(0, match[0].length),
+						kittyProtocol: true,
+					},
+					consumedLength: match[0].length,
+				}
+			}
+
 			// Handle Ctrl/Alt + letters
 			if ((ctrl || alt) && keyCode >= "a".charCodeAt(0) && keyCode <= "z".charCodeAt(0)) {
 				const letter = String.fromCharCode(keyCode)
@@ -325,8 +343,29 @@ export function mapAltKeyCharacter(char: string): string | null {
  */
 export function parseReadlineKey(key: any): Key {
 	// Handle the key object from readline
+	const keyName = key.name || (key.sequence.length === 1 ? key.sequence : "")
+
+	// Detect Shift+1/! - since readline doesn't properly detect shift for these characters,
+	// we assume '!' is always Shift+1, and we'll also check for explicit Shift+1
+	const isShift1 =
+		key.sequence === "!" ||
+		(key.sequence === "1" && key.shift) ||
+		(key.name === "exclamation" && key.shift) ||
+		(key.name === "!" && key.shift)
+
+	if (isShift1) {
+		return {
+			name: "shift-1",
+			ctrl: key.ctrl || false,
+			meta: key.meta || false,
+			shift: true,
+			paste: false,
+			sequence: key.sequence || "",
+		}
+	}
+
 	return {
-		name: key.name || (key.sequence.length === 1 ? key.sequence : ""),
+		name: keyName,
 		ctrl: key.ctrl || false,
 		meta: key.meta || false,
 		shift: key.shift || false,

+ 4 - 3
cli/turbo.json

@@ -8,18 +8,19 @@
 		},
 		"clean:kilocode": {
 			"outputs": ["dist/kilocode/**"],
-			"inputs": ["../../bin-unpacked/extension/**"]
+			"inputs": ["../bin-unpacked/extension/**"],
+			"dependsOn": ["clean"]
 		},
 		"copy:kilocode": {
 			"outputs": ["dist/kilocode/**"],
-			"inputs": ["../../bin-unpacked/extension/**"],
+			"inputs": ["../bin-unpacked/extension/**"],
 			"dependsOn": ["clean:kilocode", "kilo-code#vsix:unpacked"]
 		},
 		"build": {
 			"cache": false,
 			"outputs": ["dist/**"],
 			"inputs": ["src/**"],
-			"dependsOn": ["clean", "copy:kilocode"]
+			"dependsOn": ["copy:kilocode"]
 		},
 		"start": {
 			"cache": false,

+ 3 - 2
jetbrains/host/package.json

@@ -69,7 +69,8 @@
 	},
 	"devDependencies": {
 		"@roo-code/config-eslint": "file:../../packages/config-eslint",
-		"@playwright/test": "^1.50.0",
+		"@playwright/test": "^1.56.1",
+		"patch-package": "^8.0.0",
 		"@types/cookie": "^0.3.3",
 		"@types/debug": "^4.1.5",
 		"@types/gulp-svgmin": "^1.2.1",
@@ -134,7 +135,7 @@
 		"istanbul-lib-report": "^3.0.0",
 		"istanbul-lib-source-maps": "^4.0.1",
 		"istanbul-reports": "^3.1.5",
-		"lazy.js": "^0.4.2",
+		"lazy.js": "^0.5.1",
 		"merge-options": "^1.0.1",
 		"mime": "^1.4.1",
 		"minimatch": "^3.0.4",

File diff suppressed because it is too large
+ 254 - 221
jetbrains/host/pnpm-lock.yaml


+ 2 - 2
package.json

@@ -59,9 +59,9 @@
 		"only-allow": "^1.2.1",
 		"ovsx": "0.10.4",
 		"prettier": "^3.4.2",
-		"rimraf": "^6.0.1",
+		"rimraf": "^6.1.0",
 		"tsx": "^4.19.3",
-		"turbo": "^2.5.6",
+		"turbo": "^2.6.0",
 		"typescript": "^5.4.5"
 	},
 	"lint-staged": {

+ 1 - 2
packages/evals/package.json

@@ -38,7 +38,7 @@
 		"p-queue": "^8.1.0",
 		"p-wait-for": "^5.0.2",
 		"postgres": "^3.4.7",
-		"ps-tree": "^1.2.0",
+		"ps-list": "^8.1.1",
 		"redis": "^5.5.5",
 		"zod": "^3.25.61"
 	},
@@ -47,7 +47,6 @@
 		"@roo-code/config-typescript": "workspace:^",
 		"@types/node": "20.x",
 		"@types/node-ipc": "^9.2.3",
-		"@types/ps-tree": "^1.1.6",
 		"drizzle-kit": "^0.31.1",
 		"tsx": "^4.19.3",
 		"vitest": "^3.2.3"

+ 17 - 10
packages/evals/src/cli/runUnitTest.ts

@@ -1,13 +1,28 @@
 import * as path from "path"
 
 import { execa, parseCommandString } from "execa"
-import psTree from "ps-tree"
+import psList from "ps-list"
 
 import type { Task } from "../db/index.js"
 import { type ExerciseLanguage, EVALS_REPO_PATH } from "../exercises/index.js"
 
 import { Logger } from "./utils.js"
 
+// kilocode_change start
+/**
+ * Get child process IDs for a given parent PID
+ */
+async function getChildPids(parentPid: number): Promise<number[]> {
+	try {
+		const processes = await psList()
+		return processes.filter((p) => p.ppid === parentPid).map((p) => p.pid)
+	} catch (error) {
+		console.error(`Failed to get child processes for PID ${parentPid}:`, error)
+		return []
+	}
+}
+// kilocode_change end
+
 const UNIT_TEST_TIMEOUT = 2 * 60 * 1_000
 
 const testCommands: Record<ExerciseLanguage, { commands: string[]; timeout?: number }> = {
@@ -38,15 +53,7 @@ export const runUnitTest = async ({ task, logger }: RunUnitTestOptions) => {
 			subprocess.stderr.pipe(process.stderr)
 
 			const timeout = setTimeout(async () => {
-				const descendants = await new Promise<number[]>((resolve, reject) => {
-					psTree(subprocess.pid!, (err, children) => {
-						if (err) {
-							reject(err)
-						}
-
-						resolve(children.map((p) => parseInt(p.PID)))
-					})
-				})
+				const descendants = await getChildPids(subprocess.pid!) // kilocode_change
 
 				logger.info(
 					`"${command.join(" ")}" timed out, killing ${subprocess.pid} + ${JSON.stringify(descendants)}`,

+ 1 - 116
packages/types/src/__tests__/kilocode.test.ts

@@ -1,9 +1,8 @@
 // npx vitest run src/__tests__/kilocode.test.ts
 
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
+import { describe, it, expect, vi, afterEach } from "vitest"
 import {
 	ghostServiceSettingsSchema,
-	checkKilocodeBalance,
 	getAppUrl,
 	getKiloUrlFromToken,
 	getExtensionConfigUrl,
@@ -15,7 +14,6 @@ describe("ghostServiceSettingsSchema", () => {
 			enableAutoTrigger: true,
 			enableQuickInlineTaskKeybinding: false,
 			enableSmartInlineTaskKeybinding: true,
-			showGutterAnimation: false,
 		})
 		expect(result.success).toBe(true)
 	})
@@ -25,7 +23,6 @@ describe("ghostServiceSettingsSchema", () => {
 			enableAutoTrigger: true,
 			enableQuickInlineTaskKeybinding: true,
 			enableSmartInlineTaskKeybinding: true,
-			showGutterAnimation: true,
 		})
 		expect(result.success).toBe(true)
 	})
@@ -38,118 +35,6 @@ describe("ghostServiceSettingsSchema", () => {
 	})
 })
 
-describe("checkKilocodeBalance", () => {
-	const mockToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbnYiOiJwcm9kdWN0aW9uIn0.test"
-	const mockOrgId = "org-123"
-
-	beforeEach(() => {
-		global.fetch = vi.fn()
-	})
-
-	afterEach(() => {
-		vi.restoreAllMocks()
-	})
-
-	it("should return true when balance is positive", async () => {
-		vi.mocked(global.fetch).mockResolvedValueOnce({
-			ok: true,
-			json: async () => ({ balance: 100 }),
-		} as Response)
-
-		const result = await checkKilocodeBalance(mockToken)
-		expect(result).toBe(true)
-		expect(global.fetch).toHaveBeenCalledWith(
-			"https://api.kilocode.ai/api/profile/balance",
-			expect.objectContaining({
-				headers: expect.objectContaining({
-					Authorization: `Bearer ${mockToken}`,
-				}),
-			}),
-		)
-	})
-
-	it("should return false when balance is zero", async () => {
-		vi.mocked(global.fetch).mockResolvedValueOnce({
-			ok: true,
-			json: async () => ({ balance: 0 }),
-		} as Response)
-
-		const result = await checkKilocodeBalance(mockToken)
-		expect(result).toBe(false)
-	})
-
-	it("should return false when balance is negative", async () => {
-		vi.mocked(global.fetch).mockResolvedValueOnce({
-			ok: true,
-			json: async () => ({ balance: -10 }),
-		} as Response)
-
-		const result = await checkKilocodeBalance(mockToken)
-		expect(result).toBe(false)
-	})
-
-	it("should include organization ID in headers when provided", async () => {
-		vi.mocked(global.fetch).mockResolvedValueOnce({
-			ok: true,
-			json: async () => ({ balance: 100 }),
-		} as Response)
-
-		const result = await checkKilocodeBalance(mockToken, mockOrgId)
-		expect(result).toBe(true)
-		expect(global.fetch).toHaveBeenCalledWith(
-			"https://api.kilocode.ai/api/profile/balance",
-			expect.objectContaining({
-				headers: expect.objectContaining({
-					Authorization: `Bearer ${mockToken}`,
-					"X-KiloCode-OrganizationId": mockOrgId,
-				}),
-			}),
-		)
-	})
-
-	it("should not include organization ID in headers when not provided", async () => {
-		vi.mocked(global.fetch).mockResolvedValueOnce({
-			ok: true,
-			json: async () => ({ balance: 100 }),
-		} as Response)
-
-		await checkKilocodeBalance(mockToken)
-
-		const fetchCall = vi.mocked(global.fetch).mock.calls[0]
-		expect(fetchCall).toBeDefined()
-		const headers = (fetchCall![1] as RequestInit)?.headers as Record<string, string>
-
-		expect(headers).toHaveProperty("Authorization")
-		expect(headers).not.toHaveProperty("X-KiloCode-OrganizationId")
-	})
-
-	it("should return false when API request fails", async () => {
-		vi.mocked(global.fetch).mockResolvedValueOnce({
-			ok: false,
-		} as Response)
-
-		const result = await checkKilocodeBalance(mockToken)
-		expect(result).toBe(false)
-	})
-
-	it("should return false when fetch throws an error", async () => {
-		vi.mocked(global.fetch).mockRejectedValueOnce(new Error("Network error"))
-
-		const result = await checkKilocodeBalance(mockToken)
-		expect(result).toBe(false)
-	})
-
-	it("should handle missing balance field in response", async () => {
-		vi.mocked(global.fetch).mockResolvedValueOnce({
-			ok: true,
-			json: async () => ({}),
-		} as Response)
-
-		const result = await checkKilocodeBalance(mockToken)
-		expect(result).toBe(false)
-	})
-})
-
 describe("URL functions", () => {
 	const originalEnv = process.env.KILOCODE_BACKEND_BASE_URL
 

+ 1 - 0
packages/types/src/global-settings.ts

@@ -230,6 +230,7 @@ export const SECRET_STATE_KEYS = [
 	"codeIndexOpenAiKey",
 	"codeIndexQdrantApiKey",
 	// kilocode_change start
+	"minimaxApiKey",
 	"kilocodeToken",
 	"syntheticApiKey",
 	"ovhCloudAiEndpointsApiKey",

+ 0 - 84
packages/types/src/kilocode/kilocode.ts

@@ -1,5 +1,4 @@
 import { z } from "zod"
-import { ProviderSettings, ProviderSettingsEntry } from "../provider-settings.js"
 
 declare global {
 	interface Window {
@@ -12,7 +11,6 @@ export const ghostServiceSettingsSchema = z
 		enableAutoTrigger: z.boolean().optional(),
 		enableQuickInlineTaskKeybinding: z.boolean().optional(),
 		enableSmartInlineTaskKeybinding: z.boolean().optional(),
-		showGutterAnimation: z.boolean().optional(),
 		useNewAutocomplete: z.boolean().optional(),
 		provider: z.string().optional(),
 		model: z.string().optional(),
@@ -118,85 +116,3 @@ export function getExtensionConfigUrl(): string {
 		return "https://api.kilocode.ai/extension-config.json"
 	}
 }
-
-/**
- * Check if the Kilocode account has a positive balance
- * @param kilocodeToken - The Kilocode JWT token
- * @param kilocodeOrganizationId - Optional organization ID to include in headers
- * @returns Promise<boolean> - True if balance > 0, false otherwise
- */
-export async function checkKilocodeBalance(kilocodeToken: string, kilocodeOrganizationId?: string): Promise<boolean> {
-	try {
-		const baseUrl = getKiloBaseUriFromToken(kilocodeToken)
-
-		const headers: Record<string, string> = {
-			Authorization: `Bearer ${kilocodeToken}`,
-		}
-
-		if (kilocodeOrganizationId) {
-			headers["X-KiloCode-OrganizationId"] = kilocodeOrganizationId
-		}
-
-		const response = await fetch(`${baseUrl}/api/profile/balance`, {
-			headers,
-		})
-
-		if (!response.ok) {
-			return false
-		}
-
-		const data = await response.json()
-		const balance = data.balance ?? 0
-		return balance > 0
-	} catch (error) {
-		console.error("Error checking kilocode balance:", error)
-		return false
-	}
-}
-
-export const AUTOCOMPLETE_PROVIDER_MODELS = {
-	mistral: "codestral-latest",
-	kilocode: "mistralai/codestral-2508",
-	openrouter: "mistralai/codestral-2508",
-	bedrock: "mistral.codestral-2508-v1:0",
-} as const
-export type AutocompleteProviderKey = keyof typeof AUTOCOMPLETE_PROVIDER_MODELS
-
-interface ProviderSettingsManager {
-	listConfig(): Promise<ProviderSettingsEntry[]>
-	getProfile(params: { id: string }): Promise<ProviderSettings>
-}
-
-export type ProviderUsabilityChecker = (
-	provider: AutocompleteProviderKey,
-	providerSettingsManager: ProviderSettingsManager,
-) => Promise<boolean>
-
-export const defaultProviderUsabilityChecker: ProviderUsabilityChecker = async (provider, providerSettingsManager) => {
-	if (provider === "kilocode") {
-		try {
-			const profiles = await providerSettingsManager.listConfig()
-			const kilocodeProfile = profiles.find((p) => p.apiProvider === "kilocode")
-
-			if (!kilocodeProfile) {
-				return false
-			}
-
-			const profile = await providerSettingsManager.getProfile({ id: kilocodeProfile.id })
-			const kilocodeToken = profile.kilocodeToken
-			const kilocodeOrgId = profile.kilocodeOrganizationId
-
-			if (!kilocodeToken) {
-				return false
-			}
-
-			return await checkKilocodeBalance(kilocodeToken, kilocodeOrgId)
-		} catch (error) {
-			console.error("Error checking kilocode balance:", error)
-			return false
-		}
-	}
-
-	// For all other providers, assume they are usable
-	return true
-}

+ 4 - 0
packages/types/src/kilocode/native-function-calling.ts

@@ -19,7 +19,11 @@ export const nativeFunctionCallingProviders = [
 	"zai",
 	"synthetic",
 	"human-relay",
+	"qwen-code",
 	"inception",
+	"minimax",
+	"anthropic",
+	"moonshot",
 ] satisfies ProviderName[] as ProviderName[]
 
 const modelsDefaultingToJsonKeywords = ["claude-haiku-4.5", "claude-haiku-4-5"]

+ 1 - 0
packages/types/src/message.ts

@@ -44,6 +44,7 @@ export const clineAsks = [
 	"invalid_model",
 	"report_bug",
 	"condense",
+	"checkpoint_restore", // Added for checkpoint restore approval
 	// kilocode_change end
 ] as const
 

+ 22 - 3
packages/types/src/provider-settings.ts

@@ -14,6 +14,7 @@ import {
 	fireworksModels,
 	// kilocode_change start
 	syntheticModels,
+	minimaxModels,
 	// geminiModels,
 	// kilocode_change end
 	groqModels,
@@ -147,6 +148,7 @@ export const providerNames = [
 	"roo",
 	// kilocode_change start
 	"kilocode",
+	"minimax",
 	"gemini-cli",
 	"virtual-quota-fallback",
 	"synthetic",
@@ -415,6 +417,13 @@ const sambaNovaSchema = apiModelIdProviderModelSchema.extend({
 })
 
 // kilocode_change start
+const minimaxSchema = apiModelIdProviderModelSchema.extend({
+	minimaxBaseUrl: z
+		.union([z.literal("https://api.minimax.io/anthropic"), z.literal("https://api.minimaxi.com/anthropic")])
+		.optional(),
+	minimaxApiKey: z.string().optional(),
+})
+
 const inceptionSchema = apiModelIdProviderModelSchema.extend({
 	inceptionLabsBaseUrl: z.string().optional(),
 	inceptionLabsApiKey: z.string().optional(),
@@ -528,6 +537,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
 	fakeAiSchema.merge(z.object({ apiProvider: z.literal("fake-ai") })),
 	xaiSchema.merge(z.object({ apiProvider: z.literal("xai") })),
 	// kilocode_change start
+	minimaxSchema.merge(z.object({ apiProvider: z.literal("minimax") })),
 	geminiCliSchema.merge(z.object({ apiProvider: z.literal("gemini-cli") })),
 	kilocodeSchema.merge(z.object({ apiProvider: z.literal("kilocode") })),
 	virtualQuotaFallbackSchema.merge(z.object({ apiProvider: z.literal("virtual-quota-fallback") })),
@@ -570,6 +580,7 @@ export const providerSettingsSchema = z.object({
 	...syntheticSchema.shape,
 	...ovhcloudSchema.shape,
 	...inceptionSchema.shape,
+	...minimaxSchema.shape,
 	// kilocode_change end
 	...openAiNativeSchema.shape,
 	...mistralSchema.shape,
@@ -672,6 +683,12 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
 	unbound: "unboundModelId",
 	requesty: "requestyModelId",
 	xai: "apiModelId",
+	// kilocode_change start
+	minimax: "apiModelId",
+	synthetic: "apiModelId",
+	ovhcloud: "ovhCloudAiEndpointsModelId",
+	inception: "inceptionLabsModelId",
+	// kilocode_change end
 	groq: "apiModelId",
 	chutes: "apiModelId",
 	litellm: "litellmModelId",
@@ -680,15 +697,12 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
 	sambanova: "apiModelId",
 	zai: "apiModelId",
 	fireworks: "apiModelId",
-	synthetic: "apiModelId", // kilocode_change
 	featherless: "apiModelId",
 	"io-intelligence": "ioIntelligenceModelId",
 	roo: "apiModelId",
 	"vercel-ai-gateway": "vercelAiGatewayModelId",
 	kilocode: "kilocodeModel",
 	"virtual-quota-fallback": "apiModelId",
-	ovhcloud: "ovhCloudAiEndpointsModelId", // kilocode_change
-	inception: "inceptionLabsModelId", // kilocode_change
 }
 
 /**
@@ -822,6 +836,11 @@ export const MODELS_BY_PROVIDER: Record<
 	unbound: { id: "unbound", label: "Unbound", models: [] },
 
 	// kilocode_change start
+	minimax: {
+		id: "minimax",
+		label: "MiniMax",
+		models: Object.keys(minimaxModels),
+	},
 	ovhcloud: { id: "ovhcloud", label: "OVHcloud AI Endpoints", models: [] },
 	inception: { id: "inception", label: "Inception", models: [] },
 	kilocode: { id: "kilocode", label: "Kilocode", models: [] },

+ 13 - 0
packages/types/src/providers/fireworks.ts

@@ -10,6 +10,7 @@ export type FireworksModelId =
 	| "accounts/fireworks/models/deepseek-v3p1"
 	| "accounts/fireworks/models/glm-4p5"
 	| "accounts/fireworks/models/glm-4p5-air"
+	| "accounts/fireworks/models/glm-4p6" // kilocode_change
 	| "accounts/fireworks/models/gpt-oss-20b"
 	| "accounts/fireworks/models/gpt-oss-120b"
 
@@ -105,6 +106,18 @@ export const fireworksModels = {
 		description:
 			"Z.ai GLM-4.5-Air with 106B total parameters and 12B active parameters. Features unified reasoning, coding, and intelligent agent capabilities.",
 	},
+	// kilocode_change start
+	"accounts/fireworks/models/glm-4p6": {
+		maxTokens: 25344,
+		contextWindow: 198000,
+		supportsImages: false,
+		supportsPromptCache: false,
+		inputPrice: 0.55,
+		outputPrice: 2.19,
+		description:
+			"Z.ai's GLM-4.6 model achieves SOTA among open-source models! Context window expanded to 200K. Brings you superior performance in real-world coding, reasoning, tool using and role-playing.",
+	},
+	// kilocode_change end
 	"accounts/fireworks/models/gpt-oss-20b": {
 		maxTokens: 16384,
 		contextWindow: 128000,

+ 1 - 0
packages/types/src/providers/index.ts

@@ -13,6 +13,7 @@ export * from "./gemini-cli.js"
 export * from "./ovhcloud.js"
 export * from "./synthetic.js"
 export * from "./inception.js"
+export * from "./minimax.js"
 // kilocode_change end
 export * from "./glama.js"
 export * from "./groq.js"

+ 39 - 0
packages/types/src/providers/minimax.ts

@@ -0,0 +1,39 @@
+// kilocode_change - file added
+import type { ModelInfo } from "../model.js"
+
+// Minimax
+// https://platform.minimax.io/docs/guides/pricing
+// https://platform.minimax.io/docs/api-reference/text-openai-api
+// https://platform.minimax.io/docs/api-reference/text-anthropic-api
+export type MinimaxModelId = keyof typeof minimaxModels
+export const minimaxDefaultModelId: MinimaxModelId = "MiniMax-M2"
+
+export const minimaxModels = {
+	"MiniMax-M2": {
+		maxTokens: 16_384,
+		contextWindow: 192_000,
+		supportsImages: false,
+		supportsPromptCache: true,
+		inputPrice: 0.3,
+		outputPrice: 1.2,
+		cacheWritesPrice: 0.375,
+		cacheReadsPrice: 0.03,
+		description:
+			"MiniMax M2, a model born for Agents and code, featuring Top-tier Coding Capabilities, Powerful Agentic Performance, and Ultimate Cost-Effectiveness & Speed.",
+	},
+	"MiniMax-M2-Stable": {
+		maxTokens: 16_384,
+		contextWindow: 192_000,
+		supportsImages: false,
+		supportsPromptCache: true,
+		inputPrice: 0.3,
+		outputPrice: 1.2,
+		cacheWritesPrice: 0.375,
+		cacheReadsPrice: 0.03,
+		description:
+			"MiniMax M2 Stable (High Concurrency, Commercial Use), a model born for Agents and code, featuring Top-tier Coding Capabilities, Powerful Agentic Performance, and Ultimate Cost-Effectiveness & Speed.",
+	},
+} as const satisfies Record<string, ModelInfo>
+
+export const MINIMAX_DEFAULT_TEMPERATURE = 1.0
+export const MINIMAX_DEFAULT_MAX_TOKENS = 16384

+ 24 - 1
packages/types/src/providers/moonshot.ts

@@ -3,7 +3,7 @@ import type { ModelInfo } from "../model.js"
 // https://platform.moonshot.ai/
 export type MoonshotModelId = keyof typeof moonshotModels
 
-export const moonshotDefaultModelId: MoonshotModelId = "kimi-k2-0905-preview"
+export const moonshotDefaultModelId: MoonshotModelId = "kimi-k2-thinking"
 
 export const moonshotModels = {
 	"kimi-k2-0711-preview": {
@@ -39,6 +39,29 @@ export const moonshotModels = {
 		cacheReadsPrice: 0.6, // $0.60 per million tokens (cache hit)
 		description: `Kimi K2 Turbo is a high-speed version of the state-of-the-art Kimi K2 mixture-of-experts (MoE) language model, with the same 32 billion activated parameters and 1 trillion total parameters, optimized for output speeds of up to 60 tokens per second, peaking at 100 tokens per second.`,
 	},
+
+	"kimi-k2-thinking-turbo": {
+		maxTokens: 32_000,
+		contextWindow: 262144,
+		supportsImages: false,
+		supportsPromptCache: true,
+		inputPrice: 1.15, // $1.15 per million tokens (cache miss)
+		outputPrice: 8.0, // $8.00 per million tokens
+		cacheWritesPrice: 0, // $0 per million tokens (cache miss)
+		cacheReadsPrice: 0.15, // $0.15 per million tokens (cache hit)
+		description: `High-speed version of kimi-k2-thinking, suitable for scenarios requiring both deep reasoning and extremely fast responses`,
+	},
+	"kimi-k2-thinking": {
+		maxTokens: 32_000,
+		contextWindow: 262144,
+		supportsImages: false,
+		supportsPromptCache: true,
+		inputPrice: 0.6, // $0.6 per million tokens (cache miss)
+		outputPrice: 2.5, // $2.50 per million tokens
+		cacheWritesPrice: 0, // $0 per million tokens (cache miss)
+		cacheReadsPrice: 0.15, // $0.15 per million tokens (cache hit)
+		description: `A thinking model with general agentic and reasoning capabilities, specializing in deep reasoning tasks`,
+	},
 } as const satisfies Record<string, ModelInfo>
 
 export const MOONSHOT_DEFAULT_TEMPERATURE = 0.6

+ 11 - 0
packages/types/src/providers/synthetic.ts

@@ -8,6 +8,7 @@ export type SyntheticModelId =
 	| "hf:zai-org/GLM-4.5"
 	| "hf:openai/gpt-oss-120b"
 	| "hf:moonshotai/Kimi-K2-Instruct-0905"
+	| "hf:moonshotai/Kimi-K2-Thinking"
 	| "hf:reissbaker/llama-3.1-70b-abliterated-lora"
 	| "hf:meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8"
 	| "hf:deepseek-ai/DeepSeek-V3.1"
@@ -37,6 +38,16 @@ export const syntheticModels = {
 		description:
 			"MiniMax's latest hybrid reasoning model: it's fast, it thinks before it responds, it's great at using tools via the API, and it's a strong coding model. 192k-token context.",
 	},
+	"hf:moonshotai/Kimi-K2-Thinking": {
+		maxTokens: 262144,
+		contextWindow: 262144,
+		supportsImages: false,
+		supportsPromptCache: false,
+		inputPrice: 0.55,
+		outputPrice: 2.19,
+		description:
+			"Moonshot's latest hybrid reasoner. Extremely good at math — it saturates the AIME25 math benchmark — and competitive with GPT-5 and Claude 4.5 at tool use and codegen. 256k-token context.",
+	},
 	"hf:moonshotai/Kimi-K2-Instruct-0905": {
 		maxTokens: 262144,
 		contextWindow: 262144,

File diff suppressed because it is too large
+ 199 - 163
pnpm-lock.yaml


+ 26 - 4
scripts/reset-kilocode-state.sh

@@ -1,16 +1,38 @@
 #!/bin/sh
 
-echo "Kilocode state is being reset.  This probably doesn't work while VS Code is running."
+# Accept optional parameter for VS Code directory
+# Can be: full path, relative path, or just directory name (default: "Code")
+# Examples:
+#   ./reset-kilocode-state.sh                          # uses default "Code"
+#   ./reset-kilocode-state.sh VSCodium                 # uses "VSCodium"
+#   ./reset-kilocode-state.sh "Code - Insiders"        # uses "Code - Insiders"
+#   ./reset-kilocode-state.sh ~/custom/path            # uses full path
+
+VSCODE_DIR="${1:-Code}"
+
+# Expand ~ to $HOME if present
+VSCODE_DIR="${VSCODE_DIR/#\~/$HOME}"
+
+# If the path exists as a directory, use it directly
+# Otherwise, treat it as a directory name under ~/Library/Application Support/
+if [[ -d "$VSCODE_DIR" ]]; then
+    VSCODE_DIR="$VSCODE_DIR"
+else
+    VSCODE_DIR="$HOME/Library/Application Support/$VSCODE_DIR"
+fi
+
+echo "Kilocode state is being reset for: $VSCODE_DIR"
+echo "This probably doesn't work while VS Code is running."
 
 # Reset the secrets:
-sqlite3 ~/Library/Application\ Support/Code/User/globalStorage/state.vscdb \
+sqlite3 "$VSCODE_DIR/User/globalStorage/state.vscdb" \
 "DELETE FROM ItemTable WHERE \
     key = 'kilocode.kilo-code' OR \
     key LIKE 'workbench.view.extension.kilo-code%' OR \
     key LIKE 'secret://{\"extensionId\":\"kilocode.kilo-code\",%';"
 
 # delete all kilocode state files:
-rm -rf ~/Library/Application\ Support/Code/User/globalStorage/kilocode.kilo-code/
+rm -rf "$VSCODE_DIR/User/globalStorage/kilocode.kilo-code/"
 
 # clear some of the vscode cache that I've observed contains kilocode related entries:
-rm -f ~/Library/Application\ Support/Code/CachedProfilesData/__default__profile__/extensions.user.cache
+rm -f "$VSCODE_DIR/CachedProfilesData/__default__profile__/extensions.user.cache"

+ 5 - 2
src/api/index.ts

@@ -33,6 +33,9 @@ import {
 	// kilocode_change start
 	VirtualQuotaFallbackHandler,
 	GeminiCliHandler,
+	SyntheticHandler,
+	OVHcloudAIEndpointsHandler,
+	MiniMaxHandler,
 	// kilocode_change end
 	ClaudeCodeHandler,
 	QwenCodeHandler,
@@ -41,12 +44,10 @@ import {
 	DoubaoHandler,
 	ZAiHandler,
 	FireworksHandler,
-	SyntheticHandler, // kilocode_change
 	RooHandler,
 	FeatherlessHandler,
 	VercelAiGatewayHandler,
 	DeepInfraHandler,
-	OVHcloudAIEndpointsHandler, // kilocode_change
 } from "./providers"
 // kilocode_change start
 import { KilocodeOpenrouterHandler } from "./providers/kilocode-openrouter"
@@ -198,6 +199,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
 			return new InceptionLabsHandler(options)
 		case "ovhcloud":
 			return new OVHcloudAIEndpointsHandler(options)
+		case "minimax":
+			return new MiniMaxHandler(options)
 		// kilocode_change end
 		case "io-intelligence":
 			return new IOIntelligenceHandler(options)

+ 94 - 0
src/api/providers/__tests__/kilocode-openrouter.spec.ts

@@ -10,6 +10,12 @@ import OpenAI from "openai"
 import { KilocodeOpenrouterHandler } from "../kilocode-openrouter"
 import { ApiHandlerOptions } from "../../../shared/api"
 import { X_KILOCODE_TASKID, X_KILOCODE_ORGANIZATIONID, X_KILOCODE_PROJECTID } from "../../../shared/kilocode/headers"
+import { streamSse } from "../../../services/continuedev/core/fetch/stream"
+
+// Mock the stream module
+vitest.mock("../../../services/continuedev/core/fetch/stream", () => ({
+	streamSse: vitest.fn(),
+}))
 
 // Mock dependencies
 vitest.mock("openai")
@@ -196,4 +202,92 @@ describe("KilocodeOpenrouterHandler", () => {
 			)
 		})
 	})
+
+	describe("FIM support", () => {
+		it("supportsFim returns true for codestral models", () => {
+			const handler = new KilocodeOpenrouterHandler({
+				...mockOptions,
+				kilocodeModel: "mistral/codestral-latest",
+			})
+
+			expect(handler.supportsFim()).toBe(true)
+		})
+
+		it("supportsFim returns false for non-codestral models", () => {
+			const handler = new KilocodeOpenrouterHandler({
+				...mockOptions,
+				kilocodeModel: "anthropic/claude-sonnet-4",
+			})
+
+			expect(handler.supportsFim()).toBe(false)
+		})
+
+		it("completeFim handles errors correctly", async () => {
+			const handler = new KilocodeOpenrouterHandler({
+				...mockOptions,
+				kilocodeModel: "mistral/codestral-latest",
+			})
+
+			const mockResponse = {
+				ok: false,
+				status: 500,
+				statusText: "Internal Server Error",
+				text: vitest.fn().mockResolvedValue("Error details"),
+			}
+
+			global.fetch = vitest.fn().mockResolvedValue(mockResponse)
+
+			await expect(handler.completeFim("prefix", "suffix")).rejects.toThrow(
+				"FIM streaming failed: 500 Internal Server Error - Error details",
+			)
+		})
+
+		it("streamFim yields chunks correctly", async () => {
+			const handler = new KilocodeOpenrouterHandler({
+				...mockOptions,
+				kilocodeModel: "mistral/codestral-latest",
+			})
+
+			// Mock streamSse to return the expected data
+			;(streamSse as any).mockImplementation(async function* () {
+				yield { choices: [{ delta: { content: "chunk1" } }] }
+				yield { choices: [{ delta: { content: "chunk2" } }] }
+			})
+
+			const mockResponse = {
+				ok: true,
+				status: 200,
+				statusText: "OK",
+			} as Response
+
+			global.fetch = vitest.fn().mockResolvedValue(mockResponse)
+
+			const chunks: string[] = []
+			for await (const chunk of handler.streamFim("prefix", "suffix")) {
+				chunks.push(chunk)
+			}
+
+			expect(chunks).toEqual(["chunk1", "chunk2"])
+			expect(streamSse).toHaveBeenCalledWith(mockResponse)
+		})
+
+		it("streamFim handles errors correctly", async () => {
+			const handler = new KilocodeOpenrouterHandler({
+				...mockOptions,
+				kilocodeModel: "mistral/codestral-latest",
+			})
+
+			const mockResponse = {
+				ok: false,
+				status: 400,
+				statusText: "Bad Request",
+				text: vitest.fn().mockResolvedValue("Invalid request"),
+			}
+
+			global.fetch = vitest.fn().mockResolvedValue(mockResponse)
+
+			const generator = handler.streamFim("prefix", "suffix")
+			await expect(generator.next()).rejects.toThrow("FIM streaming failed: 400 Bad Request - Invalid request")
+		})
+	})
 })

+ 298 - 0
src/api/providers/__tests__/minimax.spec.ts

@@ -0,0 +1,298 @@
+// kilocode_change - file added
+// npx vitest run src/api/providers/__tests__/minimax.spec.ts
+
+import { MiniMaxAnthropicHandler } from "../minimax-anthropic"
+import { ApiHandlerOptions } from "../../../shared/api"
+import {
+	minimaxDefaultModelId,
+	minimaxModels,
+	MINIMAX_DEFAULT_TEMPERATURE,
+	MINIMAX_DEFAULT_MAX_TOKENS,
+} from "@roo-code/types"
+
+const mockCreate = vitest.fn()
+
+vitest.mock("@anthropic-ai/sdk", () => {
+	const mockAnthropicConstructor = vitest.fn().mockImplementation(() => ({
+		messages: {
+			create: mockCreate.mockImplementation(async (options) => {
+				if (!options.stream) {
+					return {
+						id: "test-completion",
+						content: [{ type: "text", text: "Test response from MiniMax" }],
+						role: "assistant",
+						model: options.model,
+						usage: {
+							input_tokens: 10,
+							output_tokens: 5,
+						},
+					}
+				}
+				return {
+					async *[Symbol.asyncIterator]() {
+						yield {
+							type: "message_start",
+							message: {
+								usage: {
+									input_tokens: 100,
+									output_tokens: 50,
+									cache_creation_input_tokens: 20,
+									cache_read_input_tokens: 10,
+								},
+							},
+						}
+						yield {
+							type: "content_block_start",
+							index: 0,
+							content_block: {
+								type: "text",
+								text: "Hello",
+							},
+						}
+						yield {
+							type: "content_block_delta",
+							delta: {
+								type: "text_delta",
+								text: " from MiniMax",
+							},
+						}
+					},
+				}
+			}),
+		},
+	}))
+
+	return {
+		Anthropic: mockAnthropicConstructor,
+	}
+})
+
+// Import after mock
+import { Anthropic } from "@anthropic-ai/sdk"
+
+const mockAnthropicConstructor = vitest.mocked(Anthropic)
+
+describe("MiniMaxHandler", () => {
+	let handler: MiniMaxAnthropicHandler
+	let mockOptions: ApiHandlerOptions
+
+	beforeEach(() => {
+		vitest.clearAllMocks()
+		mockOptions = {
+			minimaxApiKey: "test-minimax-api-key",
+			apiModelId: minimaxDefaultModelId,
+		}
+		handler = new MiniMaxAnthropicHandler(mockOptions)
+	})
+
+	describe("constructor", () => {
+		it("should initialize with provided options", () => {
+			expect(handler).toBeInstanceOf(MiniMaxAnthropicHandler)
+			expect(handler.getModel().id).toBe(minimaxDefaultModelId)
+		})
+
+		it("should use default international base URL", () => {
+			new MiniMaxAnthropicHandler(mockOptions)
+			expect(mockAnthropicConstructor).toHaveBeenCalledWith(
+				expect.objectContaining({
+					baseURL: "https://api.minimax.io/anthropic",
+					apiKey: "test-minimax-api-key",
+				}),
+			)
+		})
+
+		it("should use custom base URL if provided", () => {
+			const customBaseUrl = "https://api.minimaxi.com/anthropic"
+			new MiniMaxAnthropicHandler({
+				...mockOptions,
+				minimaxBaseUrl: customBaseUrl,
+			})
+			expect(mockAnthropicConstructor).toHaveBeenCalledWith(
+				expect.objectContaining({
+					baseURL: customBaseUrl,
+				}),
+			)
+		})
+
+		it("should use China base URL when provided", () => {
+			const chinaBaseUrl = "https://api.minimaxi.com/anthropic"
+			new MiniMaxAnthropicHandler({
+				...mockOptions,
+				minimaxBaseUrl: chinaBaseUrl,
+			})
+			expect(mockAnthropicConstructor).toHaveBeenCalledWith(
+				expect.objectContaining({
+					baseURL: chinaBaseUrl,
+					apiKey: "test-minimax-api-key",
+				}),
+			)
+		})
+
+		it("should initialize without API key", () => {
+			const handlerWithoutKey = new MiniMaxAnthropicHandler({
+				...mockOptions,
+				minimaxApiKey: undefined,
+			})
+			expect(handlerWithoutKey).toBeInstanceOf(MiniMaxAnthropicHandler)
+		})
+	})
+
+	describe("createMessage", () => {
+		const systemPrompt = "You are a helpful assistant."
+
+		it("should stream messages successfully", async () => {
+			const stream = handler.createMessage(systemPrompt, [
+				{
+					role: "user",
+					content: [{ type: "text" as const, text: "Hello MiniMax" }],
+				},
+			])
+
+			const chunks: any[] = []
+			for await (const chunk of stream) {
+				chunks.push(chunk)
+			}
+
+			// Verify usage information
+			const usageChunk = chunks.find((chunk) => chunk.type === "usage")
+			expect(usageChunk).toBeDefined()
+			expect(usageChunk?.inputTokens).toBe(100)
+			expect(usageChunk?.outputTokens).toBe(50)
+
+			// Verify text content
+			const textChunks = chunks.filter((chunk) => chunk.type === "text")
+			expect(textChunks).toHaveLength(2)
+			expect(textChunks[0].text).toBe("Hello")
+			expect(textChunks[1].text).toBe(" from MiniMax")
+
+			// Verify API call
+			expect(mockCreate).toHaveBeenCalledWith(
+				expect.objectContaining({
+					model: minimaxDefaultModelId,
+					max_tokens: 16384,
+					temperature: MINIMAX_DEFAULT_TEMPERATURE,
+					system: [{ text: systemPrompt, type: "text" }],
+					stream: true,
+				}),
+			)
+		})
+
+		it("should handle multiple messages", async () => {
+			const stream = handler.createMessage(systemPrompt, [
+				{
+					role: "user",
+					content: [{ type: "text" as const, text: "First message" }],
+				},
+				{
+					role: "assistant",
+					content: [{ type: "text" as const, text: "Response" }],
+				},
+				{
+					role: "user",
+					content: [{ type: "text" as const, text: "Second message" }],
+				},
+			])
+
+			const chunks: any[] = []
+			for await (const chunk of stream) {
+				chunks.push(chunk)
+			}
+
+			expect(chunks.length).toBeGreaterThan(0)
+			expect(mockCreate).toHaveBeenCalled()
+		})
+	})
+
+	describe("completePrompt", () => {
+		it("should complete prompt successfully", async () => {
+			const result = await handler.completePrompt("Test prompt")
+			expect(result).toBe("Test response from MiniMax")
+			expect(mockCreate).toHaveBeenCalledWith({
+				model: minimaxDefaultModelId,
+				messages: [{ role: "user", content: "Test prompt" }],
+				max_tokens: MINIMAX_DEFAULT_MAX_TOKENS,
+				temperature: MINIMAX_DEFAULT_TEMPERATURE,
+				thinking: undefined,
+				stream: false,
+			})
+		})
+
+		it("should handle API errors", async () => {
+			mockCreate.mockRejectedValueOnce(new Error("MiniMax API Error"))
+			await expect(handler.completePrompt("Test prompt")).rejects.toThrow("MiniMax API Error")
+		})
+
+		it("should handle non-text content", async () => {
+			mockCreate.mockImplementationOnce(async () => ({
+				content: [{ type: "image" }],
+			}))
+			const result = await handler.completePrompt("Test prompt")
+			expect(result).toBe("")
+		})
+
+		it("should handle empty response", async () => {
+			mockCreate.mockImplementationOnce(async () => ({
+				content: [{ type: "text", text: "" }],
+			}))
+			const result = await handler.completePrompt("Test prompt")
+			expect(result).toBe("")
+		})
+	})
+
+	describe("getModel", () => {
+		it("should return default model if no model ID is provided", () => {
+			const handlerWithoutModel = new MiniMaxAnthropicHandler({
+				...mockOptions,
+				apiModelId: undefined,
+			})
+			const model = handlerWithoutModel.getModel()
+			expect(model.id).toBe(minimaxDefaultModelId)
+			expect(model.info).toBeDefined()
+		})
+
+		it("should return MiniMax-M2 as default model", () => {
+			const model = handler.getModel()
+			expect(model.id).toBe("MiniMax-M2")
+			expect(model.info).toEqual(minimaxModels["MiniMax-M2"])
+		})
+
+		it("should return correct model configuration for MiniMax-M2", () => {
+			const model = handler.getModel()
+			expect(model.id).toBe("MiniMax-M2")
+			expect(model.info.maxTokens).toBe(16384)
+			expect(model.info.contextWindow).toBe(192_000)
+			expect(model.info.supportsImages).toBe(false)
+			expect(model.info.supportsPromptCache).toBe(true)
+			expect(model.info.inputPrice).toBe(0.3)
+			expect(model.info.outputPrice).toBe(1.2)
+		})
+
+		it("should use correct default max tokens", () => {
+			const model = handler.getModel()
+			expect(model.maxTokens).toBe(16384)
+		})
+	})
+
+	describe("Model Configuration", () => {
+		it("should have correct model configuration", () => {
+			expect(minimaxDefaultModelId).toBe("MiniMax-M2")
+			expect(minimaxModels["MiniMax-M2"]).toEqual({
+				maxTokens: 16384,
+				contextWindow: 192_000,
+				supportsImages: false,
+				supportsPromptCache: true,
+				inputPrice: 0.3,
+				outputPrice: 1.2,
+				cacheWritesPrice: 0.375,
+				cacheReadsPrice: 0.03,
+				description:
+					"MiniMax M2, a model born for Agents and code, featuring Top-tier Coding Capabilities, Powerful Agentic Performance, and Ultimate Cost-Effectiveness & Speed.",
+			})
+		})
+
+		it("should have correct default constants", () => {
+			expect(MINIMAX_DEFAULT_TEMPERATURE).toBe(1.0)
+			expect(MINIMAX_DEFAULT_MAX_TOKENS).toBe(16384)
+		})
+	})
+})

+ 1 - 1
src/api/providers/__tests__/moonshot.spec.ts

@@ -148,7 +148,7 @@ describe("MoonshotHandler", () => {
 			const model = handler.getModel()
 			expect(model.id).toBe(mockOptions.apiModelId)
 			expect(model.info).toBeDefined()
-			expect(model.info.maxTokens).toBe(16384)
+			expect(model.info.maxTokens).toBe(32000)
 			expect(model.info.contextWindow).toBe(262144)
 			expect(model.info.supportsImages).toBe(false)
 			expect(model.info.supportsPromptCache).toBe(true) // Should be true now

+ 90 - 2
src/api/providers/anthropic.ts

@@ -18,6 +18,7 @@ import { getModelParams } from "../transform/model-params"
 import { BaseProvider } from "./base-provider"
 import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
 import { calculateApiCostAnthropic } from "../../shared/cost"
+import { convertOpenAIToolsToAnthropic } from "./kilocode/nativeToolCallHelpers"
 
 export class AnthropicHandler extends BaseProvider implements SingleCompletionHandler {
 	private options: ApiHandlerOptions
@@ -53,6 +54,14 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
 			betas.push("context-1m-2025-08-07")
 		}
 
+		// kilocode_change start
+		const tools =
+			(metadata?.allowedTools ?? []).length > 0
+				? convertOpenAIToolsToAnthropic(metadata?.allowedTools)
+				: undefined
+		const tool_choice = (tools ?? []).length > 0 ? { type: "auto" as const } : undefined
+		// kilocode_change end
+
 		switch (modelId) {
 			case "claude-sonnet-4-5":
 			case "claude-sonnet-4-20250514":
@@ -107,6 +116,10 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
 							return message
 						}),
 						stream: true,
+						// kilocode_change start
+						tools,
+						tool_choice,
+						// kilocode_change end
 					},
 					(() => {
 						// prompt caching: https://x.com/alexalbert__/status/1823751995901272068
@@ -135,14 +148,18 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
 				break
 			}
 			default: {
-				stream = (await this.client.messages.create({
+				stream = await this.client.messages.create({
 					model: modelId,
 					max_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,
 					temperature,
 					system: [{ text: systemPrompt, type: "text" }],
 					messages,
 					stream: true,
-				})) as any
+					// kilocode_change start
+					tools,
+					tool_choice,
+					// kilocode_change end
+				}) // kilocode_change removed: as any
 				break
 			}
 		}
@@ -152,6 +169,13 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
 		let cacheWriteTokens = 0
 		let cacheReadTokens = 0
 
+		// kilocode_change start
+		let thinkingDeltaAccumulator = ""
+		let thinkText = ""
+		let thinkSignature = ""
+		const lastStartedToolCall = { id: "", name: "", arguments: "" }
+		// kilocode_change end
+
 		for await (const chunk of stream) {
 			switch (chunk.type) {
 				case "message_start": {
@@ -201,7 +225,41 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
 							}
 
 							yield { type: "reasoning", text: chunk.content_block.thinking }
+
+							// kilocode_change start
+							thinkText = chunk.content_block.thinking
+							thinkSignature = chunk.content_block.signature
+							if (thinkText && thinkSignature) {
+								yield {
+									type: "ant_thinking",
+									thinking: thinkText,
+									signature: thinkSignature,
+								}
+							}
+							// kilocode_change end
+
 							break
+
+						// kilocode_change start
+						case "redacted_thinking":
+							yield {
+								type: "reasoning",
+								text: "[Redacted thinking block]",
+							}
+							yield {
+								type: "ant_redacted_thinking",
+								data: chunk.content_block.data,
+							}
+							break
+						case "tool_use":
+							if (chunk.content_block.id && chunk.content_block.name) {
+								lastStartedToolCall.id = chunk.content_block.id
+								lastStartedToolCall.name = chunk.content_block.name
+								lastStartedToolCall.arguments = ""
+							}
+							break
+						// kilocode_change end
+
 						case "text":
 							// We may receive multiple text blocks, in which
 							// case just insert a line break between them.
@@ -217,7 +275,37 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
 					switch (chunk.delta.type) {
 						case "thinking_delta":
 							yield { type: "reasoning", text: chunk.delta.thinking }
+							thinkingDeltaAccumulator += chunk.delta.thinking // kilocode_change
+							break
+
+						// kilocode_change start
+						case "signature_delta":
+							if (thinkingDeltaAccumulator && chunk.delta.signature) {
+								yield {
+									type: "ant_thinking",
+									thinking: thinkingDeltaAccumulator,
+									signature: chunk.delta.signature,
+								}
+							}
 							break
+						case "input_json_delta":
+							if (lastStartedToolCall.id && lastStartedToolCall.name && chunk.delta.partial_json) {
+								yield {
+									type: "native_tool_calls",
+									toolCalls: [
+										{
+											id: lastStartedToolCall?.id,
+											function: {
+												name: lastStartedToolCall?.name,
+												arguments: chunk.delta.partial_json,
+											},
+										},
+									],
+								}
+							}
+							break
+						// kilocode_change end
+
 						case "text_delta":
 							yield { type: "text", text: chunk.delta.text }
 							break

+ 2 - 1
src/api/providers/index.ts

@@ -21,17 +21,18 @@ export { OllamaHandler } from "./ollama"
 export { OpenAiNativeHandler } from "./openai-native"
 export { OpenAiHandler } from "./openai"
 export { OpenRouterHandler } from "./openrouter"
-export { OVHcloudAIEndpointsHandler } from "./ovhcloud" // kilocode_change
 export { QwenCodeHandler } from "./qwen-code"
 export { RequestyHandler } from "./requesty"
 export { SambaNovaHandler } from "./sambanova"
 export { UnboundHandler } from "./unbound"
 export { VertexHandler } from "./vertex"
 // kilocode_change start
+export { OVHcloudAIEndpointsHandler } from "./ovhcloud"
 export { GeminiCliHandler } from "./gemini-cli"
 export { VirtualQuotaFallbackHandler } from "./virtual-quota-fallback"
 export { SyntheticHandler } from "./synthetic"
 export { InceptionLabsHandler } from "./inception"
+export { MiniMaxAnthropicHandler as MiniMaxHandler } from "./minimax-anthropic"
 // kilocode_change end
 export { VsCodeLmHandler } from "./vscode-lm"
 export { XAIHandler } from "./xai"

+ 62 - 4
src/api/providers/kilocode-openrouter.ts

@@ -13,6 +13,8 @@ import {
 	X_KILOCODE_PROJECTID,
 	X_KILOCODE_TESTER,
 } from "../../shared/kilocode/headers"
+import { DEFAULT_HEADERS } from "./constants"
+import { streamSse } from "../../services/continuedev/core/fetch/stream"
 
 /**
  * A custom OpenRouter handler that overrides the getModel function
@@ -21,22 +23,24 @@ import {
 export class KilocodeOpenrouterHandler extends OpenRouterHandler {
 	protected override models: ModelRecord = {}
 	defaultModel: string = openRouterDefaultModelId
+	private apiFIMBase: string
 
 	protected override get providerName() {
 		return "KiloCode" as const
 	}
 
 	constructor(options: ApiHandlerOptions) {
+		const baseApiUrl = getKiloUrlFromToken("https://api.kilocode.ai/api/", options.kilocodeToken ?? "")
+
 		options = {
 			...options,
-			openRouterBaseUrl: getKiloUrlFromToken(
-				"https://api.kilocode.ai/api/openrouter/",
-				options.kilocodeToken ?? "",
-			),
+			openRouterBaseUrl: `${baseApiUrl}openrouter/`,
 			openRouterApiKey: options.kilocodeToken,
 		}
 
 		super(options)
+
+		this.apiFIMBase = baseApiUrl
 	}
 
 	override customRequestOptions(metadata?: ApiHandlerCreateMessageMetadata) {
@@ -126,4 +130,58 @@ export class KilocodeOpenrouterHandler extends OpenRouterHandler {
 		this.defaultModel = defaultModel
 		return this.getModel()
 	}
+
+	supportsFim(): boolean {
+		const modelId = this.options.kilocodeModel ?? this.defaultModel
+		return modelId.includes("codestral")
+	}
+
+	async completeFim(prefix: string, suffix: string, taskId?: string): Promise<string> {
+		let result = ""
+		for await (const chunk of this.streamFim(prefix, suffix, taskId)) {
+			result += chunk
+		}
+		return result
+	}
+
+	async *streamFim(prefix: string, suffix: string, taskId?: string): AsyncGenerator<string> {
+		const model = await this.fetchModel()
+		const endpoint = new URL("fim/completions", this.apiFIMBase)
+
+		// Build headers using customRequestOptions for consistency
+		const headers: Record<string, string> = {
+			...DEFAULT_HEADERS,
+			"Content-Type": "application/json",
+			Accept: "application/json",
+			"x-api-key": this.options.kilocodeToken ?? "",
+			Authorization: `Bearer ${this.options.kilocodeToken}`,
+			...this.customRequestOptions(taskId ? { taskId, mode: "code" } : undefined)?.headers,
+		}
+		const max_max_tokens = 1000
+		const response = await fetch(endpoint, {
+			method: "POST",
+			body: JSON.stringify({
+				model: model.id,
+				prompt: prefix,
+				suffix,
+				max_tokens: Math.min(max_max_tokens, model.maxTokens ?? max_max_tokens),
+				temperature: model.temperature,
+				top_p: model.topP,
+				stream: true,
+			}),
+			headers,
+		})
+
+		if (!response.ok) {
+			const errorText = await response.text()
+			throw new Error(`FIM streaming failed: ${response.status} ${response.statusText} - ${errorText}`)
+		}
+
+		for await (const data of streamSse(response)) {
+			const content = data.choices?.[0]?.delta?.content
+			if (content) {
+				yield content
+			}
+		}
+	}
 }

+ 39 - 0
src/api/providers/kilocode/IFimProvider.ts

@@ -0,0 +1,39 @@
+import { ApiHandlerCreateMessageMetadata } from "../.."
+
+/**
+ * Interface for FIM (Fill-In-the-Middle) completion providers.
+ * This interface defines the contract for providers that support FIM operations,
+ * allowing for code completion between a prefix and suffix.
+ */
+export interface IFimProvider {
+	/**
+	 * Check if the provider supports FIM operations
+	 * @returns true if FIM is supported, false otherwise
+	 */
+	supportsFim(): boolean
+
+	/**
+	 * Complete code between a prefix and suffix (non-streaming)
+	 * @param prefix - The code before the cursor/insertion point
+	 * @param suffix - The code after the cursor/insertion point
+	 * @param taskId - Optional task ID for tracking
+	 * @returns The completed code string
+	 */
+	completeFim(prefix: string, suffix: string, taskId?: string): Promise<string>
+
+	/**
+	 * Stream code completion between a prefix and suffix
+	 * @param prefix - The code before the cursor/insertion point
+	 * @param suffix - The code after the cursor/insertion point
+	 * @param taskId - Optional task ID for tracking
+	 * @returns An async generator yielding code chunks
+	 */
+	streamFim(prefix: string, suffix: string, taskId?: string): AsyncGenerator<string>
+
+	/**
+	 * Get custom request options for API calls
+	 * @param metadata - Optional metadata including taskId, projectId, and mode
+	 * @returns Custom request options with headers
+	 */
+	customRequestOptions(metadata?: ApiHandlerCreateMessageMetadata): { headers: Record<string, string> } | undefined
+}

+ 16 - 0
src/api/providers/kilocode/nativeToolCallHelpers.ts

@@ -73,6 +73,7 @@ import OpenAI from "openai"
 import type { ApiHandlerCreateMessageMetadata } from "../../index"
 import type { ApiStreamNativeToolCallsChunk } from "../../transform/kilocode/api-stream-native-tool-calls-chunk"
 import { getActiveToolUseStyle, ProviderSettings, ToolUseStyle } from "@roo-code/types"
+import Anthropic from "@anthropic-ai/sdk"
 
 /**
  * Adds native tool call parameters to OpenAI chat completion params when toolStyle is "json"
@@ -141,3 +142,18 @@ export function* processNativeToolCallsFromDelta(
 		}
 	}
 }
+
+export function convertOpenAIToolsToAnthropic(allowedTools?: OpenAI.Chat.ChatCompletionTool[]): Anthropic.ToolUnion[] {
+	if (!allowedTools) return []
+
+	return allowedTools
+		.filter((tool) => tool.type === "function" && "function" in tool && !!tool.function)
+		.map((tool) => {
+			const func = (tool as any).function
+			return {
+				name: func.name,
+				description: func.description || "",
+				input_schema: func.parameters || { type: "object", properties: {} },
+			}
+		})
+}

+ 253 - 0
src/api/providers/minimax-anthropic.ts

@@ -0,0 +1,253 @@
+// kilocode_change - file added
+import { Anthropic } from "@anthropic-ai/sdk"
+import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
+
+import {
+	type ModelInfo,
+	MINIMAX_DEFAULT_MAX_TOKENS,
+	MINIMAX_DEFAULT_TEMPERATURE,
+	MinimaxModelId,
+	minimaxDefaultModelId,
+	minimaxModels,
+} from "@roo-code/types"
+
+import type { ApiHandlerOptions } from "../../shared/api"
+
+import { ApiStream } from "../transform/stream"
+import { getModelParams } from "../transform/model-params"
+
+import { BaseProvider } from "./base-provider"
+import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
+import { calculateApiCostAnthropic } from "../../shared/cost"
+import { convertOpenAIToolsToAnthropic } from "./kilocode/nativeToolCallHelpers"
+
+export class MiniMaxAnthropicHandler extends BaseProvider implements SingleCompletionHandler {
+	private options: ApiHandlerOptions
+	private client: Anthropic
+
+	constructor(options: ApiHandlerOptions) {
+		super()
+		this.options = options
+
+		this.client = new Anthropic({
+			baseURL: this.options.minimaxBaseUrl || "https://api.minimax.io/anthropic",
+			apiKey: this.options.minimaxApiKey,
+		})
+	}
+
+	async *createMessage(
+		systemPrompt: string,
+		messages: Anthropic.Messages.MessageParam[],
+		metadata?: ApiHandlerCreateMessageMetadata,
+	): ApiStream {
+		let stream: AnthropicStream<Anthropic.Messages.RawMessageStreamEvent>
+		let { id: modelId, maxTokens } = this.getModel()
+
+		const tools =
+			(metadata?.allowedTools ?? []).length > 0
+				? convertOpenAIToolsToAnthropic(metadata?.allowedTools)
+				: undefined
+		const tool_choice = (tools ?? []).length > 0 ? { type: "any" as const } : undefined
+
+		stream = await this.client.messages.create({
+			model: modelId,
+			max_tokens: maxTokens ?? MINIMAX_DEFAULT_MAX_TOKENS,
+			temperature: MINIMAX_DEFAULT_TEMPERATURE,
+			system: [{ text: systemPrompt, type: "text" }],
+			messages,
+			stream: true,
+			tools,
+			tool_choice,
+		})
+
+		let inputTokens = 0
+		let outputTokens = 0
+		let cacheWriteTokens = 0
+		let cacheReadTokens = 0
+		let thinkingDeltaAccumulator = ""
+		let thinkText = ""
+		let thinkSignature = ""
+		const lastStartedToolCall = { id: "", name: "", arguments: "" }
+		for await (const chunk of stream) {
+			switch (chunk.type) {
+				case "message_start": {
+					// Tells us cache reads/writes/input/output.
+					const {
+						input_tokens = 0,
+						output_tokens = 0,
+						cache_creation_input_tokens,
+						cache_read_input_tokens,
+					} = chunk.message.usage
+
+					yield {
+						type: "usage",
+						inputTokens: input_tokens,
+						outputTokens: output_tokens,
+						cacheWriteTokens: cache_creation_input_tokens || undefined,
+						cacheReadTokens: cache_read_input_tokens || undefined,
+					}
+
+					inputTokens += input_tokens
+					outputTokens += output_tokens
+					cacheWriteTokens += cache_creation_input_tokens || 0
+					cacheReadTokens += cache_read_input_tokens || 0
+
+					break
+				}
+				case "message_delta":
+					// Tells us stop_reason, stop_sequence, and output tokens
+					// along the way and at the end of the message.
+					yield {
+						type: "usage",
+						inputTokens: 0,
+						outputTokens: chunk.usage.output_tokens || 0,
+					}
+
+					break
+				case "message_stop":
+					// No usage data, just an indicator that the message is done.
+					break
+				case "content_block_start":
+					switch (chunk.content_block.type) {
+						case "thinking":
+							// We may receive multiple text blocks, in which
+							// case just insert a line break between them.
+							if (chunk.index > 0) {
+								yield { type: "reasoning", text: "\n" }
+							}
+
+							yield { type: "reasoning", text: chunk.content_block.thinking }
+							thinkText = chunk.content_block.thinking
+							thinkSignature = chunk.content_block.signature
+							if (thinkText && thinkSignature) {
+								yield {
+									type: "ant_thinking",
+									thinking: thinkText,
+									signature: thinkSignature,
+								}
+							}
+							break
+						case "redacted_thinking":
+							yield {
+								type: "reasoning",
+								text: "[Redacted thinking block]",
+							}
+							yield {
+								type: "ant_redacted_thinking",
+								data: chunk.content_block.data,
+							}
+							break
+						case "tool_use":
+							if (chunk.content_block.id && chunk.content_block.name) {
+								lastStartedToolCall.id = chunk.content_block.id
+								lastStartedToolCall.name = chunk.content_block.name
+								lastStartedToolCall.arguments = ""
+							}
+							break
+						case "text":
+							// We may receive multiple text blocks, in which
+							// case just insert a line break between them.
+							if (chunk.index > 0) {
+								yield { type: "text", text: "\n" }
+							}
+
+							yield { type: "text", text: chunk.content_block.text }
+							break
+					}
+					break
+				case "content_block_delta":
+					switch (chunk.delta.type) {
+						case "thinking_delta":
+							yield { type: "reasoning", text: chunk.delta.thinking }
+							thinkingDeltaAccumulator += chunk.delta.thinking
+							break
+						case "signature_delta":
+							if (thinkingDeltaAccumulator && chunk.delta.signature) {
+								yield {
+									type: "ant_thinking",
+									thinking: thinkingDeltaAccumulator,
+									signature: chunk.delta.signature,
+								}
+							}
+							break
+						case "text_delta":
+							yield { type: "text", text: chunk.delta.text }
+							break
+						case "input_json_delta":
+							if (lastStartedToolCall.id && lastStartedToolCall.name && chunk.delta.partial_json) {
+								yield {
+									type: "native_tool_calls",
+									toolCalls: [
+										{
+											id: lastStartedToolCall?.id,
+											function: {
+												name: lastStartedToolCall?.name,
+												arguments: chunk.delta.partial_json,
+											},
+										},
+									],
+								}
+							}
+					}
+
+					break
+				case "content_block_stop":
+					break
+			}
+		}
+
+		if (inputTokens > 0 || outputTokens > 0 || cacheWriteTokens > 0 || cacheReadTokens > 0) {
+			yield {
+				type: "usage",
+				inputTokens: 0,
+				outputTokens: 0,
+				totalCost: calculateApiCostAnthropic(
+					this.getModel().info,
+					inputTokens,
+					outputTokens,
+					cacheWriteTokens,
+					cacheReadTokens,
+				),
+			}
+		}
+	}
+
+	getModel() {
+		const modelId = this.options.apiModelId
+		let id = modelId && modelId in minimaxModels ? (modelId as MinimaxModelId) : minimaxDefaultModelId
+		let info: ModelInfo = minimaxModels[id]
+
+		const params = getModelParams({
+			format: "anthropic",
+			modelId: id,
+			model: info,
+			settings: this.options,
+		})
+
+		// The `:thinking` suffix indicates that the model is a "Hybrid"
+		// reasoning model and that reasoning is required to be enabled.
+		// The actual model ID honored by Anthropic's API does not have this
+		// suffix.
+		return {
+			id,
+			info,
+			...params,
+		}
+	}
+
+	async completePrompt(prompt: string) {
+		let { id: model } = this.getModel()
+
+		const message = await this.client.messages.create({
+			model,
+			max_tokens: MINIMAX_DEFAULT_MAX_TOKENS,
+			thinking: undefined,
+			temperature: MINIMAX_DEFAULT_TEMPERATURE,
+			messages: [{ role: "user", content: prompt }],
+			stream: false,
+		})
+
+		const content = message.content.find(({ type }) => type === "text")
+		return content?.type === "text" ? content.text : ""
+	}
+}

+ 15 - 2
src/api/providers/openrouter.ts

@@ -44,6 +44,7 @@ type OpenRouterProviderParams = {
 
 import { safeJsonParse } from "../../shared/safeJsonParse"
 import { isAnyRecognizedKiloCodeError } from "../../shared/kilocode/errorUtils"
+import { ReasoningDetail } from "../transform/kilocode/reasoning-details"
 // kilocode_change end
 
 import { handleOpenAIError } from "./utils/openai-error-handler"
@@ -275,6 +276,15 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
 				}
 
 				// kilocode_change start
+
+				// OpenRouter passes reasoning details that we can pass back unmodified in api requests to preserve reasoning traces for model
+				// See: https://openrouter.ai/docs/use-cases/reasoning-tokens#preserving-reasoning-blocks
+				if (delta && "reasoning_details" in delta && delta.reasoning_details) {
+					yield {
+						type: "reasoning_details",
+						reasoning_details: delta.reasoning_details as ReasoningDetail,
+					}
+				}
 				if (delta && "reasoning_content" in delta && typeof delta.reasoning_content === "string") {
 					yield { type: "reasoning", text: delta.reasoning_content }
 				}
@@ -512,10 +522,13 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
 function makeOpenRouterErrorReadable(error: any) {
 	const metadata = error?.error?.metadata as { raw?: string; provider_name?: string } | undefined
 	const parsedJson = safeJsonParse(metadata?.raw)
-	const rawError = parsedJson as { error?: string & { message?: string }; detail?: string } | undefined
+	const rawError = parsedJson as
+		| { error?: string & { message?: string }; detail?: string; message?: string }
+		| undefined
 
 	if (error?.code !== 429 && error?.code !== 418) {
-		const errorMessage = rawError?.error?.message ?? rawError?.error ?? rawError?.detail ?? error?.message
+		const errorMessage =
+			rawError?.error?.message ?? rawError?.error ?? rawError?.detail ?? rawError?.message ?? error?.message
 		throw new Error(`${metadata?.provider_name ?? "Provider"} error: ${errorMessage ?? "unknown error"}`)
 	}
 

+ 21 - 3
src/api/providers/qwen-code.ts

@@ -4,7 +4,13 @@ import OpenAI from "openai"
 import * as os from "os"
 import * as path from "path"
 
-import { type ModelInfo, type QwenCodeModelId, qwenCodeModels, qwenCodeDefaultModelId } from "@roo-code/types"
+import {
+	type ModelInfo,
+	type QwenCodeModelId,
+	qwenCodeModels,
+	qwenCodeDefaultModelId,
+	getActiveToolUseStyle, // kilocode_change
+} from "@roo-code/types"
 
 import type { ApiHandlerOptions } from "../../shared/api"
 
@@ -12,7 +18,11 @@ import { convertToOpenAiMessages } from "../transform/openai-format"
 import { ApiStream } from "../transform/stream"
 
 import { BaseProvider } from "./base-provider"
-import type { SingleCompletionHandler } from "../index"
+import type {
+	ApiHandlerCreateMessageMetadata, // kilocode_change
+	SingleCompletionHandler,
+} from "../index"
+import { addNativeToolCallsToParams, processNativeToolCallsFromDelta } from "./kilocode/nativeToolCallHelpers"
 
 const QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai"
 const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token`
@@ -201,7 +211,11 @@ export class QwenCodeHandler extends BaseProvider implements SingleCompletionHan
 		}
 	}
 
-	override async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+	override async *createMessage(
+		systemPrompt: string,
+		messages: Anthropic.Messages.MessageParam[],
+		metadata?: ApiHandlerCreateMessageMetadata, // kilocode_change
+	): ApiStream {
 		await this.ensureAuthenticated()
 		const client = this.ensureClient()
 		const model = this.getModel()
@@ -222,6 +236,8 @@ export class QwenCodeHandler extends BaseProvider implements SingleCompletionHan
 			max_completion_tokens: model.info.maxTokens,
 		}
 
+		addNativeToolCallsToParams(requestOptions, this.options, metadata) // kilocode_change
+
 		const stream = await this.callApiWithRetry(() => client.chat.completions.create(requestOptions))
 
 		let fullContent = ""
@@ -274,6 +290,8 @@ export class QwenCodeHandler extends BaseProvider implements SingleCompletionHan
 				}
 			}
 
+			yield* processNativeToolCallsFromDelta(delta, getActiveToolUseStyle(this.options)) // kilocode_change
+
 			if (apiChunk.usage) {
 				yield {
 					type: "usage",

+ 130 - 0
src/api/transform/kilocode/reasoning-details.ts

@@ -0,0 +1,130 @@
+// Originally from Cline: https://github.com/cline/cline/blob/ba98b44504d81ea2a261a7a18bf894b4893579c3/src/core/api/transform/openai-format.ts#L181
+
+import { ProviderName } from "@roo-code/types"
+import { ApiMessage } from "../../../core/task-persistence"
+
+// Type for OpenRouter's reasoning detail elements
+// https://openrouter.ai/docs/use-cases/reasoning-tokens#streaming-response
+export type ReasoningDetail = {
+	// https://openrouter.ai/docs/use-cases/reasoning-tokens#reasoning-detail-types
+	type: string // "reasoning.summary" | "reasoning.encrypted" | "reasoning.text"
+	text?: string
+	data?: string // Encrypted reasoning data
+	signature?: string | null
+	id?: string | null // Unique identifier for the reasoning detail
+	/*
+	 The format of the reasoning detail, with possible values:
+	 	"unknown" - Format is not specified
+		"openai-responses-v1" - OpenAI responses format version 1
+		"anthropic-claude-v1" - Anthropic Claude format version 1 (default)
+	 */
+	format: string //"unknown" | "openai-responses-v1" | "anthropic-claude-v1" | "xai-responses-v1"
+	index?: number // Sequential index of the reasoning detail
+}
+
+// Helper function to convert reasoning_details array to the format OpenRouter API expects
+// Takes an array of reasoning detail objects and consolidates them by index
+export function consolidateReasoningDetails(reasoningDetails: ReasoningDetail[]): ReasoningDetail[] {
+	if (!reasoningDetails || reasoningDetails.length === 0) {
+		return []
+	}
+
+	// Group by index
+	const groupedByIndex = new Map<number, ReasoningDetail[]>()
+
+	for (const detail of reasoningDetails) {
+		const index = detail.index ?? 0
+		if (!groupedByIndex.has(index)) {
+			groupedByIndex.set(index, [])
+		}
+		groupedByIndex.get(index)!.push(detail)
+	}
+
+	// Consolidate each group
+	const consolidated: ReasoningDetail[] = []
+
+	for (const [index, details] of groupedByIndex.entries()) {
+		// Concatenate all text parts
+		let concatenatedText = ""
+		let signature: string | undefined
+		let id: string | undefined
+		let format = "unknown"
+		let type = "reasoning.text"
+
+		for (const detail of details) {
+			if (detail.text) {
+				concatenatedText += detail.text
+			}
+			// Keep the signature from the last item that has one
+			if (detail.signature) {
+				signature = detail.signature
+			}
+			// Keep the id from the last item that has one
+			if (detail.id) {
+				id = detail.id
+			}
+			// Keep format and type from any item (they should all be the same)
+			if (detail.format) {
+				format = detail.format
+			}
+			if (detail.type) {
+				type = detail.type
+			}
+		}
+
+		// Create consolidated entry for text
+		if (concatenatedText) {
+			const consolidatedEntry: ReasoningDetail = {
+				type: type,
+				text: concatenatedText,
+				signature: signature,
+				id: id,
+				format: format,
+				index: index,
+			}
+			consolidated.push(consolidatedEntry)
+		}
+
+		// For encrypted chunks (data), only keep the last one
+		let lastDataEntry: ReasoningDetail | undefined
+		for (const detail of details) {
+			if (detail.data) {
+				lastDataEntry = {
+					type: detail.type,
+					data: detail.data,
+					signature: detail.signature,
+					id: detail.id,
+					format: detail.format,
+					index: index,
+				}
+			}
+		}
+		if (lastDataEntry) {
+			consolidated.push(lastDataEntry)
+		}
+	}
+
+	return consolidated
+}
+
+const supportsReasoningDetails = ["openrouter", "kilocode"] satisfies ProviderName[] as ProviderName[]
+
+export function maybeRemoveReasoningDetails_kilocode(
+	messages: ApiMessage[],
+	provider: ProviderName | undefined,
+): ApiMessage[] {
+	if (provider && supportsReasoningDetails.includes(provider)) {
+		return messages
+	}
+	return messages
+		.map((message) => {
+			let { content } = message
+			if (Array.isArray(content)) {
+				content = content
+					.map((block) => ("reasoning_details" in block ? { ...block, reasoning_details: undefined } : block))
+					.filter((block) => block.type !== "text" || !!block.text)
+			}
+			return { ...message, content }
+		})
+		.filter((message) => !Array.isArray(message.content) || message.content.length > 0)
+}

+ 18 - 0
src/api/transform/openai-format.ts

@@ -1,5 +1,6 @@
 import { Anthropic } from "@anthropic-ai/sdk"
 import OpenAI from "openai"
+import { consolidateReasoningDetails, ReasoningDetail } from "./kilocode/reasoning-details"
 
 export function convertToOpenAiMessages(
 	anthropicMessages: Anthropic.Messages.MessageParam[],
@@ -117,7 +118,19 @@ export function convertToOpenAiMessages(
 
 				// Process non-tool messages
 				let content: string | undefined
+				const reasoningDetails = new Array<ReasoningDetail>() // kilocode_change
 				if (nonToolMessages.length > 0) {
+					// kilocode_change start
+					nonToolMessages.forEach((part) => {
+						if (part.type === "text" && "reasoning_details" in part && part.reasoning_details) {
+							if (Array.isArray(part.reasoning_details)) {
+								reasoningDetails.push(...part.reasoning_details)
+							} else {
+								reasoningDetails.push(part.reasoning_details as ReasoningDetail)
+							}
+						}
+					})
+					// kilocode_change end
 					content = nonToolMessages
 						.map((part) => {
 							if (part.type === "image") {
@@ -144,6 +157,11 @@ export function convertToOpenAiMessages(
 					content,
 					// Cannot be an empty array. API expects an array with minimum length 1, and will respond with an error if it's empty
 					tool_calls: tool_calls.length > 0 ? tool_calls : undefined,
+					// kilocode_change start
+					// @ts-ignore-next-line: property is OpenRouter-specific
+					reasoning_details:
+						reasoningDetails.length > 0 ? consolidateReasoningDetails(reasoningDetails) : undefined,
+					// kilocode_change end
 				})
 			}
 		}

Some files were not shown because too many files changed in this diff