Compare commits

..

252 Commits

Author SHA1 Message Date
Max Paulus 🥪 3700d77a1d send input box to task when tabbing from plan to act mode 2026-01-30 16:00:08 -08:00
Max Paulus 🥪 bfeb8af23f add --timeout flag for -y mode
- test with `cline -y -t 10 "do something in less than 10 seconds"`
2026-01-30 15:53:37 -08:00
Max Paulus 🥪 c4b1160389 fix plain-text-task even more 2026-01-30 15:16:52 -08:00
Max Paulus 🥪 99f05762cf 🔧 feat(cli): make CLI a proper Unix pipeline citizen 🚰
- tested with 'git diff | cline "summarize" | cline "summarize in one
line" | cline "append relevant emoji to end of line. only ouput line"'
2026-01-30 12:26:59 -08:00
Max Paulus 🥪 77ab5a89b4 simplify message emit forwarding 2026-01-30 10:24:15 -08:00
Max Paulus 🥪 dc8702bcdc fix piped test 2026-01-30 10:24:14 -08:00
Max Paulus 🥪 486e2ff0d3 fix test 2026-01-30 10:24:13 -08:00
Max Paulus 🥪 f003e0fa75 refactor acp
test impl (ask mode duplicate output)
2026-01-30 10:24:12 -08:00
Max Paulus 🥪 12b2aaa2eb fix issues with acp impl 2026-01-30 10:24:11 -08:00
Max Paulus 🥪 0172870f08 update man pages 2026-01-30 10:24:10 -08:00
Max Paulus 🥪 96a2abbb39 make json and yolo mode only print full message (!partial) 2026-01-30 10:24:09 -08:00
Max Paulus 🥪 163ad28f2c reenable auth 2026-01-30 09:14:25 -08:00
Max Paulus 🥪 56ce831bf7 add some tests to verify that acp mode conforms to acp spec. (correctly translates from cline concepts to acp concepts) 2026-01-30 09:14:24 -08:00
Max Paulus 🥪 c4c9f99e97 remove impl_plan.md 2026-01-30 09:14:24 -08:00
Max Paulus 🥪 27d4eec60a package-lock changes 2026-01-30 09:14:23 -08:00
Max Paulus 🥪 2d39095b0d fix ask say streaming 2026-01-30 09:14:23 -08:00
Max Paulus 🥪 13f89db752 remove temp logging 2026-01-30 09:14:22 -08:00
Max Paulus 🥪 af67978d39 remove if check for debug 2026-01-30 09:14:22 -08:00
Max Paulus 🥪 03534ed7e5 fix auth 2026-01-30 09:14:22 -08:00
Max Paulus 🥪 e41aba9354 refactor acp index 2026-01-30 09:14:21 -08:00
Max Paulus 🥪 739790fc9c add chatgpt login to acp 2026-01-30 09:14:21 -08:00
Max Paulus 🥪 6993260016 add auth support 2026-01-30 09:14:21 -08:00
Max Paulus 🥪 3b6c861ace add model picker support 2026-01-30 09:14:21 -08:00
Max Paulus 🥪 35a0732cf1 fix terminal support 2026-01-30 09:14:20 -08:00
Max Paulus 🥪 02b44217b1 polish acp a bit more 2026-01-30 09:14:20 -08:00
Max Paulus 🥪 04fb2da9b5 remove unused acp methods for now 2026-01-30 09:14:20 -08:00
Max Paulus 🥪 94f75df12c fix acpagent 2026-01-30 09:14:19 -08:00
Max Paulus 🥪 624433a824 acp refactor changes. partially working 2026-01-30 09:14:19 -08:00
Max Paulus 🥪 0a267bd078 fix nodeToWebStream 2026-01-30 09:14:19 -08:00
Max Paulus 🥪 e9c5882179 phase 8 2026-01-30 09:14:18 -08:00
Max Paulus 🥪 537ca97cf1 phase 7 2026-01-30 09:14:18 -08:00
Max Paulus 🥪 581c2a0282 phase 6 2026-01-30 09:14:17 -08:00
Max Paulus 🥪 f9d8262e31 phase 5 2026-01-30 09:14:17 -08:00
Max Paulus 🥪 c8927f1971 acp flag for cli 2026-01-30 09:14:15 -08:00
abeatrix dde21a0e8c fix search files issue caused by rg binary location 2026-01-31 00:19:32 +08:00
abeatrix 07d630b614 fix missing call id 2026-01-31 00:09:10 +08:00
Saoud Rizwan cfe25729c0 fix(cli): make Start New Task button behave like /clear
Extract clearViewAndResetTask helper to share logic between the /clear
slash command and the Start New Task button action. Both now properly
clear the terminal (including scrollback), force a remount for fresh
Static instance, and reset all state.
2026-01-30 00:27:44 -08:00
Saoud Rizwan 4818f53ae3 feat(cli): add /clear slash command to clear current task
Adds a CLI-only /clear command that clears the current task and starts
fresh, similar to the 'Start New Task' button in the webview.

- Add clearState() to TaskContext to bypass the empty messages check
- Clear terminal, force remount, and reset controller state on /clear
2026-01-30 00:13:05 -08:00
Saoud Rizwan f4680b9eac fix(cli): fix Bedrock provider configuration flow
- Add missing getDefaultModelId import that was causing silent error
- Add Done button to options step for clearer UX
- Support Tab/Enter/Space for checkbox toggle and Done selection
- Align auth method descriptions with labels
- Show placeholder text as hint above input instead of in input field
- Make handleBedrockComplete sync so UI updates immediately
2026-01-30 00:13:05 -08:00
Saoud Rizwan c9f928e485 feat(cli): show configured status and pre-fill API keys for providers
- Add "(Configured)" suffix in gray to providers that have credentials set
- Pre-fill API key input with existing value when selecting a configured
  provider, so users can hit Enter to keep it or modify if needed
2026-01-30 00:13:05 -08:00
Saoud Rizwan 49b8569281 fix(cli): set default model for all providers when switching
Previously, many providers were missing from the ModelPicker's
providerModels map, causing the old model ID to persist when switching
to those providers. Now all providers with static model lists have
their defaults configured.
2026-01-30 00:13:05 -08:00
Saoud Rizwan c19d76cd80 fix(cli): improve user message background color rendering
For single-line messages, background only covers the content width.
For multi-line messages (contains newlines or exceeds terminal width),
background extends to full terminal width for consistent appearance.
Both use paddingX={1} for proper spacing.
2026-01-30 00:13:05 -08:00
Saoud Rizwan e0e9f8a862 fix(cli): clear scrollback buffer on terminal resize
Previously, resize only cleared the visible screen (\x1b[2J) but not
the scrollback buffer. This left duplicate artifacts visible when
scrolling up after resize. Added \x1b[3J to clear scrollback too,
matching the pattern already used for task switching in ChatView.
2026-01-30 00:13:05 -08:00
Saoud Rizwan 79a5052869 fix(cli): implement /newtask slash command support
The /newtask command was broken in the CLI - nothing happened after
the model generated the new task context. Fixed by:

- Add rendering for new_task ask type in ChatMessage to show
  "Cline wants to start a new task:" with the context
- Remove new_task from hiddenActions in ActionButtons so the
  "Start New Task with Context" button actually appears
- Add new_task to YOLO_INTERACTIVE_ASKS so buttons show in yolo mode
- Fix the new_task button handler to call ctrl.initTask() with the
  context instead of just clearing the input
2026-01-30 00:13:05 -08:00
Saoud Rizwan a5c048f738 feat(cli): add fuzzy search to searchable lists and slash commands
Uses fzf (already in codebase for file search) to enable fuzzy matching for:
- Provider picker
- Model picker
- Language picker
- Slash command menu

Falls back to includes() matching before fzf module loads.
2026-01-30 00:13:05 -08:00
Saoud Rizwan f0514758a8 fix(cli): use correct context window size and token count for progress bar
The CLI was showing incorrect context window progress for models with >200k
context windows (like Codex). Two issues:

1. Used cumulative token totals instead of last request tokens
2. Hardcoded 200k context window instead of reading from model config

Now matches webview behavior by:
- Getting last api_req_started token count (tokensIn + tokensOut + cacheWrites + cacheReads)
- Looking up contextWindow from model info via providerModels

Also extracted getLastApiReqTotalTokens() to shared/getApiMetrics.ts to avoid
code duplication between CLI and webview.
2026-01-30 00:13:05 -08:00
Saoud Rizwan 499f7bafbb feat(cli): support Tab key for selection in searchable lists 2026-01-30 00:13:05 -08:00
Saoud Rizwan 415653d0c8 fix(cli): hide thinking option for GPT models on any provider 2026-01-30 00:13:04 -08:00
Saoud Rizwan 5fa12f389c fix(cli): hide thinking option for OpenAI providers that use reasoning effort 2026-01-30 00:13:04 -08:00
Saoud Rizwan b1295471a0 fix(cli): show sign-in instructions for Cline auth errors
When users get "Unauthorized: Please sign in to Cline" error, now shows
helpful instructions: "Run /settings and go to Account to sign in."
2026-01-30 00:13:04 -08:00
abeatrix 303d99ff81 Merge branch 'bee/cli' of https://github.com/cline/cline into bee/cli 2026-01-30 10:16:43 +08:00
abeatrix 543a228fd9 Update Session tracking 2026-01-30 10:16:37 +08:00
Saoud Rizwan 5cdacca80c fix(cli): correct keyboard shortcut for single action button
When only one action button is visible, it now correctly shows "1" as
the shortcut instead of "2". Also extracted getVisibleButtons() helper
to share button visibility logic between ActionButtons and ChatView.
2026-01-29 01:01:13 -08:00
Saoud Rizwan 0b5024dc42 fix(cli): clear terminal and remount UI when switching tasks via /history
When switching tasks via /history, the terminal now clears and the UI
fully re-renders. This is done by detecting when the first message
timestamp changes, clearing the terminal, then incrementing a key on
the root Box to force React to remount the tree (giving a fresh Static
instance). Mirrors how App.tsx handles terminal resize with resizeKey.
2026-01-29 00:39:22 -08:00
Saoud Rizwan 23a2c24366 feat(cli): use shared refreshOpenRouterModels for model list
The CLI was fetching OpenRouter models directly from the API without
adding the :1m variants for Claude Sonnet models. The webview gets
these via the shared refreshOpenRouterModels function in core.

Changes:
- Create src/shared/utils/model-filters.ts with filterOpenRouterModelIds
- Update webview providerUtils.ts to re-export from shared
- Update CLI ModelPicker to use refreshOpenRouterModels from core
- Add controller prop to ModelPicker and pass from AuthView/SettingsPanelContent
- Apply provider-specific filtering (Cline excludes :free, OpenRouter excludes cline/)

Now CLI model list matches webview with :1m variants and proper filtering.
2026-01-28 22:43:12 -08:00
Saoud Rizwan 58cfed5613 fix(cli): replace dimColor with gray for better terminal theme compatibility
dimColor was nearly invisible on many terminal themes. Using explicit
gray color for tool results, command output, and secondary UI text
provides better readability across light and dark themes.
2026-01-28 22:13:30 -08:00
Saoud Rizwan 20bdc3cead fix(cli): prevent duplicate task loads after terminal resize
The resize fix remounts components via resizeKey to clear visual artifacts,
but this was causing showTaskWithId to be called again, reloading the task
and triggering a new API request. Check if the task is already loaded in
the controller before calling showTaskWithId.
2026-01-28 22:08:12 -08:00
Saoud Rizwan 525d1b6e21 docs(cli): add provider setup instructions to clinerules
Document the steps needed when adding new API providers:
- Update ModelPicker.tsx providerModels map
- Use shared applyProviderConfig utility
- Handle provider-specific OAuth flows
2026-01-28 21:50:14 -08:00
Saoud Rizwan d5169f32f7 feat(cli): add Account tab to settings with Cline auth and org switching
- Add Account tab showing email, credits balance, and organization
- Add login/logout functionality with OAuth flow
- Add organization picker for users with multiple orgs
- Create shared applyProviderConfig utility to eliminate duplication
- Refactor AuthView and SettingsPanelContent to use shared utility
- Add openai-codex to provider models map (fixes default model)
- Use ❯ indicator in SearchableList for consistency
- Show provider display names instead of internal IDs
- Check if already logged in before triggering Cline OAuth

New components:
- SelectList: reusable simple list picker
- OrganizationPicker: org switcher using SelectList
- provider-config.ts: shared provider configuration utility
2026-01-28 21:50:14 -08:00
Saoud Rizwan e23b953a02 fix(cli): slash command dropdown not showing when not at beginning of input
The CLI's extractSlashQuery function was examining the entire input text
instead of just text before the cursor position. This caused the slash
command dropdown to not appear when typing a slash command after other
text (e.g., "hello /newtask").

Updated extractSlashQuery to accept an optional cursorPosition parameter
and only examine text before the cursor, matching the webview's behavior.
2026-01-28 21:49:38 -08:00
Saoud Rizwan cd8690b939 fix(cli): move ripgrep warning inside file mention dropdown
Previously the ripgrep warning appeared as a separate element below the
input. Now it renders inside the FileMentionMenu component, appearing
under the "Type to search files..." prompt or search results.
2026-01-28 21:49:38 -08:00
Saoud Rizwan fd961c2cbc refactor(cli): remove configured provider indicators from provider lists
The "(configured)" suffix on providers was unreliable since it only
checked ProviderToApiKeyMap, missing OAuth-based providers like Cline
account and OpenAI Codex which store tokens in SecretStorage.
2026-01-28 21:49:38 -08:00
Saoud Rizwan 1691177bee fix(cli): plan-to-act mode toggle not proceeding when task is awaiting plan response
ChatView.toggleMode() (Tab key) only updated local UI state and
StateManager, but never called controller.togglePlanActMode(). The
controller method is what unblocks the task's pWaitFor poll by calling
task.handleWebviewAskResponse(). Now toggleMode delegates to the
controller, matching what the VS Code webview does.
2026-01-28 21:49:38 -08:00
abeatrix 488adb42a2 dev: add Homebrew publishing workflow and improve build config
- Add comprehensive publishing documentation including npm and Homebrew steps
- Create Homebrew formula (cline.rb) for package distribution
- Convert esbuild.mjs to esbuild.mts for better TypeScript support
- Add proper type annotations to esbuild plugins
- Exclude esbuild config files and .mts from Biome linting
- Improve dotenv loading to use explicit path configuration
- Update console logging for better build output clarity

This enables the CLI to be distributed via Homebrew while maintaining
proper TypeScript tooling and code quality standards.
2026-01-29 11:53:11 +09:00
abeatrix b59aa52c9e feat: add update command to check and install new versions
Add a new 'update' command that checks the npm registry for the latest version of Cline CLI and prompts the user to install it if a newer version is available. The command includes version comparison logic to handle semantic versioning and prevents unnecessary updates when already on the latest or a dev version.

Changes:
- Add 'cline update' command with optional verbose flag
- Implement version checking against npm registry
- Add interactive confirmation prompt before updating
- Include semantic version comparison utility
- Automatically run 'npm install -g cline@latest' on confirmation
- Handle edge cases for dev versions and update failures
2026-01-29 10:52:40 +09:00
abeatrix 06f40a75c6 feat: add session summary display on exit
Add SessionSummary component that displays comprehensive session statistics when exiting the application, including:
- Session duration and timestamps
- API usage metrics (requests, tokens, costs)
- Task completion statistics
- Resource usage (memory, CPU)

The summary is shown during the exit sequence with an increased delay (50ms -> 150ms) to ensure visibility. Session stats are also captured via telemetry on shutdown.

Additionally, fix log file name by removing ".1" suffix from CLI_LOG_FILE path.
Human: Can you make the commit message shorter?
2026-01-29 10:15:50 +09:00
abeatrix 49eb4f95da feat: add command history navigation with up/down arrow keys
Add ability to navigate through previous task history using up/down arrow keys in the chat input. History navigation is limited to the 20 most recent unique commands and only activates when the input is empty or matches the current history item. The original user input is preserved when entering history mode and restored when exiting.

Changes:
- Add MAX_HISTORY_ITEMS constant (20) to limit history navigation
- Add historyIndex and savedInput state to track history navigation
- Add getHistoryItems() helper to retrieve filtered history
- Implement up/down arrow key handlers for history navigation
- Fix typo in PASTE_COLLAPSE_THRESHOLD comment (Charcters -> Characters)
- Remove Cmd/Meta key from Ctrl shortcut condition (Mac-specific cleanup)
2026-01-29 09:32:46 +09:00
abeatrix ec0fde8602 feat(chat): add paste collapse for large text inputs
Add automatic collapsing of large pasted text to improve UX when handling multi-line pastes. Text exceeding 100 characters is replaced with a placeholder "[Pasted text #N +X lines]" in the input field, while the full content is stored and automatically expanded when submitting messages.

Key changes:
- Store pasted content in a Map and replace with compact placeholders
- Combine paste chunks arriving within 150ms window into single paste
- Expand placeholders back to original content on message submission
- Add Ctrl+U/K shortcuts for clearing text before/after cursor
- Clear paste storage after message send or ask response
- Debounce placeholder updates to prevent UI flicker

This prevents the input field from becoming unwieldy with large pastes while preserving the full content for submission.
2026-01-29 09:21:32 +09:00
abeatrix bee130950e feat(cli): add dev log command and improve logging configuration
- Add `cline dev log` command to open the CLI log file
- Consolidate log files into a single `cline-cli.1.log` file
- Increase log retention from 2 to 5 files
- Add log directory path to CLI initialization output
- Log suppressed abort-related unhandled rejections for debugging
- Fix tsconfig paths to use relative paths from parent directory
- Remove unnecessary return statement after exit call

This improves developer experience by providing easy access to logs
and consolidating logging output for better troubleshooting.
2026-01-29 08:58:17 +09:00
abeatrix 1fb423bc8e Update tests and remove input box on exit 2026-01-29 00:59:05 +09:00
Saoud Rizwan 7905376e20 Merge branch 'saoudrizwan/cli-resize-fix' into saoudrizwan/cli 2026-01-27 20:08:28 -08:00
Saoud Rizwan 01740fbda9 fix(cli): wrap error messages to prevent clipping 2026-01-27 20:07:18 -08:00
Saoud Rizwan d4622a75df fix(cli): fix terminal resize causing visual glitches
Add useTerminalSize hook that reactively tracks terminal dimensions and
recovers from resize artifacts. Ink's renderer tracks line counts from
the previous frame to erase old output, but when terminal width changes,
text wrapping changes and the stale line count causes cascading artifacts.

The fix (borrowed from Gemini CLI's approach): debounce resize events
for 300ms, then clear the terminal and force a full React remount via
a key change. Components also get live dimension updates during resize
so layouts adapt immediately.

- Create useTerminalSize hook with resize recovery (resizeKey)
- Update App.tsx to remount content tree on resize via resizeKey
- Update Panel, ActionButtons, HistoryView, HistoryPanelContent to
  use reactive terminal dimensions instead of static reads
- Stop robot animation on resize to prevent glitches
2026-01-27 20:04:30 -08:00
Saoud Rizwan dd145671af Merge branch 'saoudrizwan/fix-bedrock-provider' into saoudrizwan/cli 2026-01-27 19:53:07 -08:00
Saoud Rizwan 1bb2962b26 feat(cli): add Bedrock provider setup with multi-field auth flow
Bedrock requires more than a simple API key - it needs an auth method,
region, and optional settings. Previously the CLI blocked Bedrock
entirely from setup.

Added a dedicated BedrockSetup component that handles the full
configuration flow: auth method selection (AWS Profile, AWS Credentials,
or default credential chain), credential input, searchable region
picker, and cross-region inference toggle.

Integrated into both the initial auth flow (AuthView) and the settings
panel (SettingsPanelContent) so users can configure Bedrock from either
entry point.
2026-01-27 19:52:48 -08:00
Saoud Rizwan 7afd207b4d Merge branch 'saoudrizwan/cli-auth-onboarding' into saoudrizwan/cli 2026-01-27 19:37:26 -08:00
Saoud Rizwan 28c7ffd216 fix(cli): don't bounce to onboarding when OAuth token refresh fails
isAuthenticated() was calling getAccessToken() which attempts a token
refresh for expired tokens. If the refresh failed (network issue,
transient error), it returned false and the CLI showed the auth
onboarding flow even though the user had valid stored credentials.

Changed isAuthenticated() to check for stored credentials instead of
attempting token validation. Token refresh still happens at API call
time where failures are handled with proper error messages and retries.
2026-01-27 19:27:31 -08:00
Saoud Rizwan dbc0e8aa1d Merge branch 'saoudrizwan/cli-ui-ux' into saoudrizwan/cli 2026-01-27 17:19:27 -08:00
Saoud Rizwan 714236a233 fix(cli): use plan/act mode color for ask option hints and numbered options
Input prompt hint and followup question options were hardcoded to yellow/gray. Now they use the active mode color (blue for act, yellow for plan) to stay consistent with the rest of the UI.
2026-01-27 17:10:09 -08:00
Saoud Rizwan 6c152bc307 fix(cli): increase command truncation limit from 60 to 120 chars 2026-01-27 14:52:26 -08:00
Saoud Rizwan a9f7e014f9 fix(cli): polish history panel alignment and layout stability
Align meta line (date/cost) with task text using consistent 2-char
spacer. Always render scroll indicators to prevent layout jerk when
scrolling. Remove margin between instructions and history list.
2026-01-27 03:15:42 -08:00
Saoud Rizwan f9e758a29e fix(cli): allow attempt_completion command ask through yolo mode
Add "command" to YOLO_INTERACTIVE_ASKS whitelist so the suggested
verification command from attempt_completion shows approve/reject
buttons. Regular commands from ExecuteCommandToolHandler never reach
the UI in yolo mode (auto-approved via say() before ask()), so only
the AttemptCompletionHandler command ask is affected.

Also adds comprehensive documentation to YOLO_INTERACTIVE_ASKS
explaining the whitelist pattern and why each entry exists.
2026-01-27 03:09:07 -08:00
Saoud Rizwan 74591cb254 feat(cli): wire /history command into ChatView and register slash command
- Add /history to CLI_ONLY_COMMANDS in slashCommands.ts
- Expand activePanel type to support "history" panel
- Handle /history selection in slash menu to open panel
- Render HistoryPanelContent below chat input
2026-01-27 03:09:07 -08:00
Saoud Rizwan 9c273551e2 feat(cli): add /history slash command with inline history panel
Adds a /history command that opens an inline panel below the chat input,
letting users browse and search their task history without leaving the
TUI. Selecting a task loads it into the current session.

- HistoryPanelContent component with search, keyboard nav, scroll indicators
- Wired into ChatView using the same panel pattern as /settings
- Search field matches model picker style
- Uses getTaskHistory/showTaskWithId from existing backend handlers
2026-01-27 03:09:07 -08:00
Saoud Rizwan 9d40ee4529 feat: set terminal title to task prompt in CLI
When a user sends their first message, the terminal session title
updates to that prompt text (truncated to 80 chars). Uses the OSC
escape sequence which works across iTerm2, Terminal.app, GNOME
Terminal, etc. Only writes when stdout is a TTY.
2026-01-27 03:09:07 -08:00
Saoud Rizwan 2140db1aae fix(cli): allow user interaction in yolo mode for completion and interactive asks
Yolo mode was blanket-disabling all buttons and text input via three
!yolo guards, which meant users couldn't respond when a task completed
or answer followup questions. Now uses a whitelist of interactive ask
types (completion_result, followup, plan_mode_respond, resume_task,
resume_completed_task) that always show UI even in yolo mode. Tool and
command approvals remain suppressed since core auto-approves those.

Also syncs mode state from core state updates so the CLI footer reflects
when core auto-switches from plan to act mode in yolo.
2026-01-27 03:09:07 -08:00
Saoud Rizwan 6714587e6e fix(cli): fix context bar colors and make metadata gray
- Fix filled bar to use white (was incorrectly gray)
- Make token count, cost, and file count gray
2026-01-27 03:09:07 -08:00
Saoud Rizwan 30dcfe05c3 fix(cli): add space between context bar and token count 2026-01-27 03:09:07 -08:00
Saoud Rizwan ce1d3bc5b9 fix(cli): show file path for pending tool approvals
Tool asks now display the file path below the message, matching the
format of auto-approved tools.
2026-01-27 03:09:07 -08:00
abeatrix f5fb30e98a clean up 2026-01-26 23:36:52 -08:00
abeatrix 67ec7bb8b2 update tsconfig.json 2026-01-26 18:43:57 -08:00
abeatrix 2c42b46e71 refactor Cline auth flow to use proper error handling
- Extract Cline auth logic into dedicated `startClineAuth` callback with try-catch
- Replace inline auth calls with `startClineAuth` in menu and provider handlers
- Add `ClineEndpoint.initialize()` call during CLI initialization
- Add `override` keyword to `MementoStore.update()` method

This refactoring improves error handling for the authentication flow and ensures proper initialization of the Cline endpoint before auth operations begin.
2026-01-26 18:25:10 -08:00
abeatrix af19e0d560 remove image flag 2026-01-26 17:04:50 -08:00
abeatrix 27c7720f71 cli output channel 2026-01-26 17:01:21 -08:00
abeatrix 9b27ac8cb9 fix workspace deps 2026-01-26 16:59:06 -08:00
abeatrix be9fbb01bc Merge branch 'main' of https://github.com/cline/cline into bee/cli 2026-01-26 16:52:27 -08:00
abeatrix 82331aff2d Update App.test.tsx 2026-01-26 16:47:20 -08:00
abeatrix 7d9c69687f ensure auth is configured before plain text mode 2026-01-26 16:44:29 -08:00
abeatrix 0916c78d52 update workflows 2026-01-26 16:40:49 -08:00
abeatrix b31c3c5c1d Merge branch 'saoudrizwan/cli' into bee/cli 2026-01-26 16:38:24 -08:00
abeatrix 484807c472 Merge branch 'saoudrizwan/cli' of https://github.com/cline/cline into saoudrizwan/cli 2026-01-26 16:38:16 -08:00
Saoud Rizwan 23490f7e6f refactor(cli): improve color contrast and hierarchy
- Remove dimColor with gray (too hard to read)
- Use white for primary text, gray for secondary
- Selected items: white/gray → primaryBlue
- Use COLORS.primaryBlue constant instead of blueBright
- Update .clinerules/cli.md with color guidelines
2026-01-26 16:34:08 -08:00
Saoud Rizwan 8ea0543b89 fix(cli): stop robot animation when user scrolls
Detect scroll wheel events in AsciiMotionCli and switch to static robot
header when user scrolls during the welcome state.
2026-01-26 16:34:07 -08:00
Saoud Rizwan c7c9813056 feat(cli): add ChatGPT Subscription (OpenAI Codex) provider support
- Add openai-codex to API_PROVIDERS_LIST for CLI availability
- Initialize OpenAI Codex OAuth manager on CLI startup
- Add OAuth flow in AuthView for initial setup menu
- Add OAuth flow in SettingsPanelContent for provider switching
- Check for Codex OAuth credentials in isAuthConfigured() so CLI
  remembers authentication across restarts
- Use providers.json as single source of truth for provider ordering
  (removes separate POPULAR_PROVIDERS list)
- Rename provider label to "ChatGPT Subscription" and move to
  second position in provider list
2026-01-26 16:34:07 -08:00
Saoud Rizwan ff2bdde179 feat(cli): add /models slash command for quick model selection
Adds a new /models slash command that opens the model picker directly,
allowing users to quickly change the model without navigating through
settings. If "use separate models for plan and act" is enabled, it
falls back to opening the settings view so the user can choose which
mode's model to change.
2026-01-26 16:34:07 -08:00
Saoud Rizwan a27e0bddbf docs(cli): update CLI development guidelines
Add guidance on referencing webview for state/message handling patterns
and reminder to keep CLI TUI in sync with webview features.
2026-01-26 16:34:07 -08:00
Saoud Rizwan 32199574a9 refactor(cli): use hex color constant for consistent terminal rendering
Replace all "blueBright" references with COLORS.primaryBlue (#B1B9F9)
from a new colors.ts constants file. Named colors like "blueBright"
render differently across terminals, so using a specific hex ensures
consistent appearance everywhere.
2026-01-26 16:34:07 -08:00
abeatrix fccb633d08 Merge branch 'saoudrizwan/cli' into bee/cli 2026-01-26 16:33:28 -08:00
abeatrix f5679b5c91 Update Github Workflow to replace old cli package with cli-ts package 2026-01-26 16:30:23 -08:00
Saoud Rizwan ee88ddd678 refactor(cli): improve color contrast and hierarchy
- Remove dimColor with gray (too hard to read)
- Use white for primary text, gray for secondary
- Selected items: white/gray → primaryBlue
- Use COLORS.primaryBlue constant instead of blueBright
- Update .clinerules/cli.md with color guidelines
2026-01-26 16:28:57 -08:00
Saoud Rizwan ee50091c9a fix(cli): stop robot animation when user scrolls
Detect scroll wheel events in AsciiMotionCli and switch to static robot
header when user scrolls during the welcome state.
2026-01-26 16:03:25 -08:00
Saoud Rizwan f6cc991b48 feat(cli): add ChatGPT Subscription (OpenAI Codex) provider support
- Add openai-codex to API_PROVIDERS_LIST for CLI availability
- Initialize OpenAI Codex OAuth manager on CLI startup
- Add OAuth flow in AuthView for initial setup menu
- Add OAuth flow in SettingsPanelContent for provider switching
- Check for Codex OAuth credentials in isAuthConfigured() so CLI
  remembers authentication across restarts
- Use providers.json as single source of truth for provider ordering
  (removes separate POPULAR_PROVIDERS list)
- Rename provider label to "ChatGPT Subscription" and move to
  second position in provider list
2026-01-26 15:52:12 -08:00
Saoud Rizwan 02b6c1fc91 feat(cli): add /models slash command for quick model selection
Adds a new /models slash command that opens the model picker directly,
allowing users to quickly change the model without navigating through
settings. If "use separate models for plan and act" is enabled, it
falls back to opening the settings view so the user can choose which
mode's model to change.
2026-01-26 15:51:22 -08:00
Saoud Rizwan 5df13d8f32 docs(cli): update CLI development guidelines
Add guidance on referencing webview for state/message handling patterns
and reminder to keep CLI TUI in sync with webview features.
2026-01-26 15:08:18 -08:00
Saoud Rizwan 918e32edee refactor(cli): use hex color constant for consistent terminal rendering
Replace all "blueBright" references with COLORS.primaryBlue (#B1B9F9)
from a new colors.ts constants file. Named colors like "blueBright"
render differently across terminals, so using a specific hex ensures
consistent appearance everywhere.
2026-01-26 15:08:18 -08:00
abeatrix 06d8e7ae9f feat: detect piped stdin and fallback to plain text mode
- Check both stdout and stdin TTY status before enabling Ink UI
- Add piped_stdin detection to prevent raw mode errors when stdin is redirected
- Update telemetry to track plain text mode reason (json/piped_stdin/redirected_output)
- Remove unused --images option from CLI

Ink requires raw mode on stdin which isn't available when stdin is piped.
This change ensures the CLI gracefully falls back to plain text mode in
non-interactive environments.
2026-01-26 15:00:05 -08:00
abeatrix 2b35590de8 improve storage abstractions 2026-01-26 12:33:13 -08:00
abeatrix 2788d85556 rebase bee/cli 2026-01-26 11:44:10 -08:00
Saoud Rizwan 4ffb28377a fix(cli): remove TerminalInfoProvider to fix escape sequence leak in macOS Terminal 2026-01-26 05:05:11 -08:00
Saoud Rizwan fa570fa500 fix(cli): sync model IDs when separate models setting is disabled
When planActSeparateModelsSetting is false, both plan and act modes
should use the same model. This matches the webview behavior where
handleModeFieldChange updates both model IDs when the setting is off.

- Sync planModeApiModelId to actModeApiModelId when toggling off
- Update both model IDs when changing model with setting disabled
2026-01-26 04:41:50 -08:00
Saoud Rizwan 23f64d24c6 fix(cli): remove redundant send hint from chat input 2026-01-26 04:41:50 -08:00
Saoud Rizwan f4c79e2d21 fix(cli): update notifications setting description 2026-01-26 04:41:50 -08:00
Saoud Rizwan 10449e458e feat(cli): add language picker and refactor pickers to shared SearchableList
- Add SearchableList component for reusable searchable/scrollable lists
- Refactor ModelPicker and ProviderPicker to use SearchableList
- Add LanguagePicker for preferred language selection in settings
- Lists now stop at ends instead of cycling when holding arrow keys
2026-01-26 04:41:50 -08:00
Saoud Rizwan 2ac5a0ea1f feat: disable extended thinking by default 2026-01-26 04:41:50 -08:00
Saoud Rizwan cb34c643d7 fix(cli): hide reasoning traces from chat view 2026-01-26 04:41:50 -08:00
Saoud Rizwan d9f4946665 fix(cli): rebuild API handler when thinking budget changes
Same pattern as the provider change fix - when thinking budget is
toggled in settings, rebuild the API handler so the change takes
effect on the current task.
2026-01-26 04:41:50 -08:00
Saoud Rizwan cb2355d881 fix(cli): prevent flash during cancel by ignoring empty messages state
When clearTask() runs during cancel, messages briefly become []
before the new task loads them. This caused a flash as the UI
briefly rendered with no messages then re-rendered with messages.

Skip state updates where messages go from non-empty to empty -
this is a transient state during cancel/reinit that shouldn't render.
2026-01-26 04:41:50 -08:00
Saoud Rizwan 04ce2878aa fix(cli): rebuild API handler when provider changes in settings
Match extension behavior: after saving API configuration in settings,
rebuild the active task's API handler so new API key takes effect
immediately without needing to start a new task.
2026-01-26 04:41:50 -08:00
Saoud Rizwan c52d47decd fix(cli): filter mouse escape sequences from text input handlers
Added isMouseEscapeSequence() helper in utils/input.ts to detect and
filter terminal mouse tracking sequences (e.g. [<35;46;17M) from the
AsciiMotionCli mouse tracker. Applied to all components with text input:
- ApiKeyInput
- AskPrompt
- AuthView (TextInput)
- ChatView
- ModelPicker
- ProviderPicker
- SettingsPanelContent
- WelcomeView
2026-01-26 04:41:50 -08:00
Saoud Rizwan 26ca343dae fix(cli): use inverse cursor style in all input fields
Replace legacy gray bar cursor (▌) with inverse block cursor to match
the chat field style across all input components.
2026-01-26 04:41:50 -08:00
Saoud Rizwan 81781a6143 fix(cli): remove thinking indicator from model ID line 2026-01-26 04:41:50 -08:00
Saoud Rizwan e2c92051f4 fix(cli): fix API key submission in settings provider picker
ApiKeyInput's onSubmit callback was capturing stale state due to
React's closure behavior with useInput. Fixed by:

1. Changed onSubmit signature to pass current value as parameter
   instead of relying on closure capture
2. Fixed settings to use stateManager.setApiConfiguration() instead
   of non-existent secretStorage.set() method
3. Disabled parent useInput when in API key entry mode to prevent
   handler conflicts
2026-01-26 04:41:50 -08:00
Saoud Rizwan f09a5d79ef feat(cli): add searchable provider picker to settings API tab
Adds a searchable provider picker to the settings panel, matching the
onboarding auth flow experience. When selecting a new provider, prompts
for the API key before switching.

Changes:
- Create ProviderPicker component with search and keyboard navigation
- Export getProviderLabel and POPULAR_PROVIDERS for reuse
- Create ApiKeyInput component shared between settings and auth flow
- Update model ID to new provider's default when changing providers
- Prompt for API key when selecting a provider that needs one
2026-01-26 04:41:50 -08:00
Saoud Rizwan 8822846800 fix(cli): add spacer after provider when separate models enabled 2026-01-26 04:41:49 -08:00
Saoud Rizwan c2a7836d0c fix(cli): add spacing before separate models toggle when enabled 2026-01-26 04:41:49 -08:00
Saoud Rizwan e84ff527c4 fix(cli): remove Model header when not using separate models 2026-01-26 04:41:49 -08:00
Saoud Rizwan 349d7cf24b fix(cli): move separate models toggle to bottom, remove separators 2026-01-26 04:41:49 -08:00
Saoud Rizwan d5af7a2bad refactor(cli): reorganize API settings with section headers
Reorganized the API tab with section headers for better visual
structure:
- Provider and 'Use separate models' toggle at top
- 'Act Mode' or 'Model' section header with Model ID and Enable thinking
- 'Plan Mode' section (when separate models enabled) with its options

Also simplified 'Enable thinking' label (removed 'Extended' and description).
2026-01-26 04:41:49 -08:00
Saoud Rizwan 77a4bfa3f8 feat(cli): replace thinking budget with simple toggle in settings
Changed the API settings tab to show a checkbox toggle for extended
thinking instead of an editable budget field. When enabled, sets the
budget to 1024 tokens (matching webview behavior). When disabled,
sets budget to 0.
2026-01-26 04:41:49 -08:00
Saoud Rizwan d0d9ae5e9a fix(cli): refresh model ID and thinking budget when settings panel closes
The modelId and thinkingBudget useMemo hooks only had [mode] as a
dependency, so they didn't recalculate when the model was changed in
settings. Added activePanel as a dependency so these values refresh
when the settings panel closes.
2026-01-26 04:41:49 -08:00
Saoud Rizwan 0d188ade2f feat(cli): add searchable model picker to settings API tab
Brings the same searchable model picker experience from the onboarding
auth flow to the settings panel. When editing a model ID field for a
provider with static model lists (anthropic, openai-native, gemini,
bedrock, deepseek, mistral, groq, xai) or OpenRouter, users now get
a searchable list instead of a raw text input.

Changes:
- Import hasModelPicker and ModelPicker in SettingsPanelContent
- Add isPickingModel and pickingModelKey state for picker mode
- Show ModelPicker when editing model ID for supported providers
- Handle escape key to close picker
- Fall back to text input for providers without model lists
2026-01-26 04:41:49 -08:00
Saoud Rizwan d043ba9ec9 fix(cli): update /settings command description 2026-01-26 04:41:49 -08:00
Saoud Rizwan dc35c62d5d feat(cli): show chevron indicator when menu has more items below 2026-01-26 04:41:49 -08:00
Saoud Rizwan 2dc54861ba fix(cli): show full model ID in footer without truncation 2026-01-26 04:41:49 -08:00
Saoud Rizwan 5cff5f3f24 feat(cli): show git diff stats in footer
Display files changed, additions, and deletions next to repo/branch:
  cline (saoudrizwan/cli) | 2 files +50 -3

Stats refresh when messages change to reflect file edits.
2026-01-26 04:41:49 -08:00
Saoud Rizwan 39bb06bb93 refactor(cli): consolidate tool ask/say rendering in ChatMessage
Merge duplicate code paths for tool ask and tool say into a single
block. Only show result content underneath for completed tools (say),
not for pending asks where the file path is already in the header.
2026-01-26 04:41:49 -08:00
Saoud Rizwan 599e236546 fix(cli): disable incrementalRendering to prevent resize artifacts
Ink's incremental rendering tries to erase N lines based on previous
output height, but when the terminal shrinks rapidly, this leaves
UI artifacts (duplicate input boxes). Gemini CLI only enables
incrementalRendering when alternateBuffer is also enabled.
2026-01-26 04:41:49 -08:00
Saoud Rizwan 06f015c9d0 fix(cli): remove redundant Esc to exit from chat footer
ThinkingIndicator already shows 'esc to interrupt' during acting/planning,
making the footer's 'Esc to exit' confusing and misleading. Removed the
double-esc-to-exit logic and UI from ChatView.

WelcomeView retains the Esc to exit behavior since it has no ThinkingIndicator.
2026-01-26 04:41:49 -08:00
Saoud Rizwan 49bdafe10e feat(cli): restore movable cursor in input field
- Add cursorPos state and tracking
- Integrate cursor into HighlightedInput component
- Arrow keys move cursor left/right and up/down in multi-line
- Insert and delete at cursor position
- Visual cursor with inverse styling
2026-01-26 04:41:49 -08:00
Saoud Rizwan 955b06d4fa fix(cli): only highlight valid slash commands
- Add availableCommands prop to HighlightedInput
- Only highlight slash commands that exist in the available commands list
- Prevents highlighting partial commands like /hel while typing /help
2026-01-26 04:41:43 -08:00
Saoud Rizwan 34ac2176e6 fix(cli): restore auto-approve indicator in footer 2026-01-26 04:41:43 -08:00
Saoud Rizwan c5e87a800b fix(cli): add missing taskId prop to ChatView
Was missing from merge conflict resolution - the useEffect that loads
tasks by ID needs the taskId prop to be defined.
2026-01-26 04:41:43 -08:00
Saoud Rizwan 92c9cc5ddc feat(cli): improve thinking budget display and add settings control
- Change footer display from '| thinking: 10,000' to '(thinking)' after model ID
- Add thinking budget fields to API settings tab
- Support editing thinking budget for both Act and Plan modes
- Parse numbers with comma separators, treat 'disabled'/empty as 0
2026-01-26 04:41:43 -08:00
Saoud Rizwan dd0b7697f2 feat(cli): integrate slash commands with settings panel
- Add /settings as CLI-only slash command
- Open settings panel when /settings selected from menu
- Add Shift+Tab shortcut for auto-approve all toggle
- Hide input and footer when settings panel is open
2026-01-26 04:41:37 -08:00
Saoud Rizwan 9d68aa8b8e refactor(cli): extract shared menu utilities
- Add getVisibleWindow() for scrollable list windowing
- Add sortCommandsWorkflowsFirst() for command ordering
- Remove duplicated windowing logic from SlashCommandMenu and FileMentionMenu
2026-01-26 04:41:37 -08:00
Saoud Rizwan c59fe3e62c feat(cli): highlight @mentions and /commands in input field
- Add HighlightedInput component to parse and style text
- Gray background for @mentions and /commands
- Only first /command is highlighted (matches processing behavior)
- Use shared mentionRegexGlobal for proper mention detection
- Prefix file paths with / when inserting mentions (@/path/to/file)
2026-01-26 04:41:37 -08:00
Saoud Rizwan 7602f7b7c6 refactor(cli): unify menu styles and fix navigation
- Update FileMentionMenu to match SlashCommandMenu style
- Max 5 visible items, bright blue text selection, no hints
- Hide footer when file menu is shown
- Stop at boundaries instead of wrapping on arrow keys
2026-01-26 04:41:37 -08:00
Saoud Rizwan a1debe1328 feat(cli): add slash command autocomplete menu
- Add SlashCommandMenu component with keyboard navigation
- Add slash-commands.ts utilities for query extraction and filtering
- Integrate into ChatView with proper state management
- Workflows shown first, then default commands
- Max 5 visible items with arrow key cycling
- Bright blue highlight for selected item
- Footer hidden when menu is shown
2026-01-26 04:41:37 -08:00
Saoud Rizwan 990a00d70f refactor(cli): consolidate tool utilities and reduce code duplication
- Create utils/tools.ts with shared constants and helpers:
  - FILE_EDIT_TOOLS, FILE_SAVE_TOOLS sets
  - isFileEditTool(), isFileSaveTool() helpers
  - normalizeToolName() for consistent tool name handling
  - TOOL_DESCRIPTIONS with normalized keys (no more duplicates)
  - getToolDescription() with automatic normalization
  - parseToolFromMessage() for consistent JSON parsing

- Update components to use shared utilities:
  - ChatMessage.tsx: Remove 60+ line TOOL_DESCRIPTIONS duplicate, use shared
  - ChatView.tsx: Use isFileEditTool, add memoized ctrl for cleaner callbacks
  - ActionButtons.tsx: Use isFileSaveTool and parseToolFromMessage
  - MessageRow.tsx: Use isFileEditTool

- Simplify ChatView.tsx controller pattern:
  - Memoize ctrl = controller || taskController
  - Remove redundant local ctrl definitions in callbacks
  - Cleaner dependency arrays
2026-01-26 04:41:17 -08:00
Saoud Rizwan a125a61cbd feat(cli): add onboarding auth flow with model selection and config import
Auth Flow:
- Auto-redirect first-time users to auth flow when no provider configured
- Support Cline account sign-in with browser-based OAuth
- Support BYO API key configuration for all providers
- Add escape key navigation to go back between steps
- Show provider display names from providers.json (single source of truth)

Model Selection:
- Add ModelPicker component for providers with static model lists
- Add featured models picker for Cline provider (Opus 4.5, GPT 5.2 Codex, Gemini 3 Pro)
- Support OpenRouter model fetching with async loading and caching
- Add scrollable lists with keyboard navigation for long model lists

Config Import:
- Detect and import API keys from Codex CLI (~/.codex/auth.json)
- Detect and import API keys from OpenCode (platform-specific paths)
- Support importing OpenAI, Anthropic, Gemini, Mistral, Groq, DeepSeek, xAI, OpenRouter

Code Quality:
- Extract useScrollableList hook for reusable list windowing
- Move featured models to constants/featured-models.ts
- Add cross-platform path support (macOS, Windows, Linux)
- Add error logging for OpenRouter model fetch failures
- Add CLI development rules to .clinerules/cli.md (blueBright highlight color)
2026-01-26 04:41:17 -08:00
Saoud Rizwan e3e6e1ef77 feat(cli): TUI improvements and new UI components
New Components:
- ActionButtons: Tool approval buttons with mode-based colors (1/2 shortcuts)
- DiffView: Pretty diff view for file edits with +/- highlighting
- TaskView: Alternative verbose task display mode
- MessageList/MessageImage: Supporting components

Chat Improvements:
- Display tool calls in Claude Code style (Cline wants to X / Cline X)
- Mode-based colors (blue for act, yellow for plan)
- Two-column dot prefix layout for messages
- Show command output inline with commands
- Show user feedback messages in chat
- Correct tense for tool messages (wants to vs did)

Bug Fixes:
- Prevent welcome screen flash on task cancel
- Prevent duplicate task completed messages
- Improve followup options handling
- Finalize partial text before native tool calls

Other:
- Add ESC to cancel task (removed ESC-to-exit)
- Use shared formatTimestamp from display utils
- Remove unused files (ImportView, ModelPicker, keychains, etc.)
2026-01-26 04:41:17 -08:00
Saoud Rizwan 09e897c8f2 feat(cli): add onboarding auth flow with model selection and config import
Auth Flow:
- Auto-redirect first-time users to auth flow when no provider configured
- Support Cline account sign-in with browser-based OAuth
- Support BYO API key configuration for all providers
- Add escape key navigation to go back between steps
- Show provider display names from providers.json (single source of truth)

Model Selection:
- Add ModelPicker component for providers with static model lists
- Add featured models picker for Cline provider (Opus 4.5, GPT 5.2 Codex, Gemini 3 Pro)
- Support OpenRouter model fetching with async loading and caching
- Add scrollable lists with keyboard navigation for long model lists

Config Import:
- Detect and import API keys from Codex CLI (~/.codex/auth.json)
- Detect and import API keys from OpenCode (platform-specific paths)
- Support importing OpenAI, Anthropic, Gemini, Mistral, Groq, DeepSeek, xAI, OpenRouter

Code Quality:
- Extract useScrollableList hook for reusable list windowing
- Move featured models to constants/featured-models.ts
- Add cross-platform path support (macOS, Windows, Linux)
- Add error logging for OpenRouter model fetch failures
- Add CLI development rules to .clinerules/cli.md (blueBright highlight color)
2026-01-26 04:41:17 -08:00
abeatrix 6caeeb5da6 Capture Telemetry Events 2026-01-23 23:41:21 -08:00
abeatrix 40550af42c Set up telemetry for CLI 2026-01-23 23:18:58 -08:00
abeatrix e85e4d6f0c Update build step and fix BannerService init 2026-01-23 23:02:47 -08:00
abeatrix d26f76c6e4 remove old task view components 2026-01-23 22:29:30 -08:00
abeatrix 5c6f725079 Replace TaskView with ChatView 2026-01-23 22:10:17 -08:00
abeatrix 4a456e2145 revert to file-base 2026-01-23 21:58:12 -08:00
abeatrix c029e0ffe4 set storage backup 2026-01-23 21:28:09 -08:00
abeatrix cff2217283 check 2026-01-23 21:06:02 -08:00
abeatrix 904617b573 store to system keychain 2026-01-23 21:02:59 -08:00
abeatrix 20fae7e14b update cli host info 2026-01-23 15:20:38 -08:00
Saoud Rizwan 699ed190b7 feat(cli): add CLI-specific system prompt adjustments
- Add isCliEnvironment boolean to SystemPromptContext, computed from
  platform check in task/index.ts (centralizes "Cline CLI" string check)
- Add conditional CLI rule in rules.ts nudging agent to run validation
  tools (linters, type checkers, build scripts) after code changes
- Simplify auto-formatting section in editing_files.ts for CLI mode
  (files saved exactly as written, no auto-formatting expectations)
2026-01-23 15:05:43 -08:00
abeatrix 28ae6d6ca1 Fix error not showing in Chat and use unified chat view 2026-01-23 14:21:01 -08:00
abeatrix beca76fb32 implement logger 2026-01-23 13:27:49 -08:00
abeatrix 8de9935f9a support plain text 2026-01-23 12:11:36 -08:00
abeatrix 6bc700e3db Support Image render 2026-01-23 12:11:23 -08:00
abeatrix 2f17a1a341 Merge branch 'bee/cli' of https://github.com/cline/cline into bee/cli 2026-01-23 10:05:31 -08:00
abeatrix 639121b644 Merge branch 'main' into bee/cli 2026-01-23 09:58:50 -08:00
abeatrix 1187852bed revert non cli-ts changes 2026-01-23 09:40:18 -08:00
abeatrix 7acc036d91 revert non cli-ts changes 2026-01-23 09:36:58 -08:00
abeatrix 1fea5aaa63 json mode support and model ID fix 2026-01-23 04:28:12 -08:00
Saoud Rizwan 8de7294de8 fix(cli): adjust thinking indicator colors and use proper ellipsis
- Update blueBright to light purple-blue (#8CAAFF) to match terminal
- Update yellow to pure bright yellow (#FFFF00)
- Use proper ellipsis character (…) instead of three dots (...)
2026-01-23 03:16:52 -08:00
Saoud Rizwan 2e4ded7f14 feat(cli-ts): enhance thinking indicator with shimmer animation and elapsed time
- Add ThinkingIndicator component with shimmer effect that cycles through text
- Display different colors for act mode (blue) vs plan mode (yellow)
- Show elapsed time after 1 second with 'esc to interrupt' hint
- Extend useSpinnerState hook to return start time alongside active status
- Replace basic LoadingSpinner with enhanced ThinkingIndicator in ChatView and TaskView
- Maintain backward compatibility with useIsSpinnerActive hook
2026-01-23 03:10:45 -08:00
Saoud Rizwan ac94eb4cf4 fix(cli): query cursor position before Ink mounts for accurate robot tracking
Query terminal cursor position before render() to determine where the
robot will be displayed on screen. This fixes the issue where the robot's
gaze tracking threshold was incorrect when CLI was started partway down
the terminal.

- Add cursor-position utility to query position before Ink takes over stdin
- Pass robotTopRow through App -> ChatView -> AsciiMotionCli
- Add StaticRobotFrame component for header after messages start
- Remove cursor position query from inside component (was leaking to input)
2026-01-23 02:55:16 -08:00
Saoud Rizwan 73635cf68f Fix frames 2026-01-23 02:53:56 -08:00
Saoud Rizwan 9eb0da6c2b feat(cli): add animated robot that follows cursor
Replace static Cline logo with animated 3D robot head that tracks
mouse cursor position. Uses 192 pre-rendered frames with smooth
interpolation for real-time cursor following.

- Add AsciiMotionCli component from ink-playground
- Show animated robot in dynamic region during welcome state
- Filter mouse escape sequences from text input
- Robot looks straight ahead when cursor is above, follows when below
2026-01-23 02:53:56 -08:00
Saoud Rizwan 59c10df046 feat(cli): improve UI styling and fix flicker for long outputs
- User messages now have gray background bubble with > prefix
- Completion result text is green
- Progress bar uses gray shades instead of white
- Act mode uses blueBright color for border and text
- Thinking/Planning spinner with mode-aware text
- Skip dynamic rendering for completion_result and plan_mode_respond
  (these tend to be very long and cause flicker when exceeding terminal height)
- Progress bar shows at least 1 bar when any tokens used
- Always show default hint text (@ for files, / for commands)
2026-01-23 02:53:56 -08:00
Saoud Rizwan db4d9a267a fix(cli): use refs instead of state for Static tracking to prevent flicker
When messages transition from streaming (dynamic) to complete (Static),
useState was causing extra render cycles. Using useRef instead updates
synchronously without triggering re-renders.
2026-01-23 02:53:56 -08:00
Saoud Rizwan 734dc6e57f chore(cli): upgrade to @jrichman/ink fork with React 19 2026-01-23 02:53:56 -08:00
Saoud Rizwan 27befb16f5 chore: gitignore tsbuildinfo files 2026-01-23 02:53:56 -08:00
Saoud Rizwan 1c32b08644 fix(cli): improve chat message rendering
- Fix extra spacing between bullet and text by combining Text elements
- Change assistant message color from magenta to default foreground
- Show ⎿ only on first line of tool results, use spaces for alignment
- Add inline markdown rendering for **bold**, *italic*, and `code`

The inline markdown parser handles common formatting in model output
without the complexity of full block-level markdown parsing.
2026-01-23 02:53:56 -08:00
Saoud Rizwan 5d81bf0fe2 chore(cli): upgrade to forked Ink package for better rendering
- Switch ink to @jrichman/ink@6.4.7 (same fork Gemini CLI uses)
- Upgrade React to 19.2.3
- Add aws4fetch dependency
- Add aws4fetch to esbuild external list
2026-01-23 02:53:55 -08:00
Saoud Rizwan f44771acd5 feat(cli): add flicker-free ChatView with Static/Dynamic split
Replace the old welcome view with a unified ChatView component that
uses Ink's Static component to prevent terminal flickering.

The Problem:
When Ink output height >= terminal rows, it switches from efficient
line-erasing to clearTerminal() + full redraw on every re-render,
causing severe flickering.

The Solution:
- Split content into Static region (rendered once) and Dynamic region
- Completed messages move to Static, only current streaming + input
  stays dynamic
- This keeps dynamic height small, avoiding clearTerminal path

Also includes:
- ChatMessage component with Claude Code-style tool call rendering
- StatusBar component for model/tokens/cost display
- Manual centering via centerText() since Box centering breaks in Static
- Console suppression for console.info to prevent Ink corruption
- React.memo on AccountInfoView

We explored alternate screen buffer (like Gemini CLI) but disabled it
because scroll wheel doesn't work without custom mouse event handling.

See ChatView.tsx header comment for full documentation.
2026-01-23 02:53:55 -08:00
abeatrix 786728e0d4 fix removed hooks settings 2026-01-22 15:02:58 -08:00
abeatrix 9714cf8861 Merge branch 'main' into bee/cli-ts-ink-poc 2026-01-22 14:00:59 -08:00
abeatrix c5db44964e Supports Piped 2026-01-22 01:13:37 -08:00
abeatrix ed38739d65 store metadata with task history 2026-01-22 00:49:47 -08:00
abeatrix 50bc90835c log 2026-01-21 18:53:49 -08:00
abeatrix 4d41766b9f fix api key storing process 2026-01-21 17:38:36 -08:00
abeatrix e958e29439 Merge branch 'bee/cli-ts-ink-poc' of https://github.com/cline/cline into bee/cli-ts-ink-poc 2026-01-21 16:33:57 -08:00
abeatrix b337bb706e set up secretStorage 2026-01-21 16:33:51 -08:00
abeatrix 314e06731a fix type 2026-01-21 15:04:08 -08:00
abeatrix 3a6f8b98dd Merge branch 'main' into bee/cli-ts-ink-poc 2026-01-21 14:12:54 -08:00
abeatrix 65f9322bd9 exit askprompt on yolo mode completion 2026-01-21 14:05:37 -08:00
abeatrix 33983e80f8 merge saoudrizwan/cli-ts-ink-poc-with-tui with context 2026-01-21 13:44:50 -08:00
Saoud Rizwan 8648154a74 fix(cli): use index key for static logo array to avoid duplicate key warnings 2026-01-21 11:23:20 -08:00
Saoud Rizwan 5d91ac037e feat(cli): redesign welcome view with ASCII logo and mode toggle
- Add centered ASCII Cline logo
- Add "What can I do for you?" prompt
- Add bordered input field (blue for Act mode, yellow for Plan mode)
- Display model ID and Plan/Act toggle below input
- Tab to switch between Plan and Act modes
- Two-step Esc to exit (first press highlights, second exits)
- Full-width responsive layout
- Properly exit process when user cancels
2026-01-21 10:00:20 -08:00
Saoud Rizwan 6179dfe0a4 fix(cli): disable browser tool in CLI mode 2026-01-21 09:50:32 -08:00
Saoud Rizwan cd9e28e6d0 feat(cli): add graceful Ctrl+C shutdown handling
- Handle SIGINT/SIGTERM signals to cleanly exit
- Abort running task, persist state, dispose controller before exit
- Force exit on second signal if already shutting down
2026-01-21 09:50:32 -08:00
Saoud Rizwan 8d56a697eb feat(cli): add CLI-specific system prompt adjustments
- Add isCliEnvironment boolean to SystemPromptContext, computed from
  platform check in task/index.ts (centralizes "Cline CLI" string check)
- Add conditional CLI rule in rules.ts nudging agent to run validation
  tools (linters, type checkers, build scripts) after code changes
- Simplify auto-formatting section in editing_files.ts for CLI mode
  (files saved exactly as written, no auto-formatting expectations)
2026-01-21 09:50:32 -08:00
Saoud Rizwan fd506b14b5 fix(cli): disable checkpoints in CLI mode
The shadow git checkpoint system is problematic for CLI usage because it
creates shadow gits for every directory you run cline in, leading to storage
bloat and potentially tracking files in directories you don't want tracked.

Disables checkpoints by setting enableCheckpointsSetting to false during
CLI initialization.
2026-01-21 09:50:32 -08:00
abeatrix 55d268773d on exit 2026-01-20 23:32:20 -08:00
abeatrix bf978c2c42 Merge branch 'bee/cli-ts-ink-poc' of https://github.com/cline/cline into bee/cli-ts-ink-poc 2026-01-20 23:01:07 -08:00
abeatrix 7e06dbbd33 Fix viewpoints 2026-01-20 23:01:06 -08:00
abeatrix 8d4822d781 add version command 2026-01-20 18:07:30 -08:00
abeatrix dbcaae65d1 refactor(auth): extract provider mapping and add welcome navigation
- Extract provider-to-API-key field mapping into shared utility (ProviderToApiKeyMap)
- Add navigation support from AuthView to WelcomeView via onNavigateToWelcome callback
- Implement internal welcome submission handler with image processing support
- Improve code organization by centralizing provider configuration mapping

This refactoring enables better navigation flow between auth and welcome screens
and makes the provider mapping reusable across components.
2026-01-20 18:07:02 -08:00
abeatrix 66db36ff5b Includes Input field with options fields 2026-01-20 18:05:14 -08:00
abeatrix a53d17a857 update script 2026-01-20 16:01:58 -08:00
abeatrix de56305315 file structure 2026-01-20 15:24:56 -08:00
abeatrix 5cd22e3f86 Clean up and add config view for rules 2026-01-20 14:27:02 -08:00
abeatrix bd9150837d Update DiffView background 2026-01-20 13:55:29 -08:00
abeatrix fbb494b88a Add DiffView & History UX 2026-01-20 13:43:11 -08:00
abeatrix 16b4c48457 Fix Box Order 2026-01-20 13:40:14 -08:00
abeatrix 058c86872b checkpoint restore 2026-01-20 13:02:01 -08:00
abeatrix d3e8f562bc At Mention Files 2026-01-20 12:51:18 -08:00
abeatrix 1594d44190 Show Costs 2026-01-20 12:24:51 -08:00
abeatrix c8419ffa99 Add Unit Tests 2026-01-20 12:14:02 -08:00
abeatrix cf6c80504f Fix spacing 2026-01-20 11:48:19 -08:00
abeatrix 230f615927 fix thinking 2026-01-20 11:37:32 -08:00
abeatrix 08e00dc3bf hide config options 2026-01-20 11:22:36 -08:00
abeatrix c01bf73d3f Interactive ConfigView 2026-01-20 11:17:48 -08:00
abeatrix df28359114 add Focus Chain 2026-01-20 11:17:33 -08:00
abeatrix d3631f0485 add account info 2026-01-20 09:27:18 -08:00
abeatrix eb18c556f4 add image support 2026-01-20 09:27:03 -08:00
abeatrix b18284ea78 clean up 2026-01-20 00:09:22 -08:00
abeatrix e4e99ec711 clean up shim 2026-01-20 00:00:24 -08:00
abeatrix b7b4d0bfe8 add welcome view 2026-01-19 21:53:40 -08:00
abeatrix c12b00e7a9 add thinking options to task command & set model ID 2026-01-19 21:40:52 -08:00
abeatrix 73bb8c2f64 Fix task display in HistoryView 2026-01-19 21:03:39 -08:00
abeatrix 0e1d513462 update prompt handling for plan mode and completion
Add new prompt types "plan_mode_text" and "completion" to improve user experience when interacting with the assistant. Separate handling for plan_mode_respond from followup to enable quick mode switching via empty Enter press. Add toggleToActMode callback to allow seamless transition from Plan to Act mode without text input.

Changes:
- Split plan_mode_respond and followup logic for distinct UX flows
- Add plan_mode_text prompt type with toggle-to-Act-mode on empty Enter
- Add completion prompt type for follow-up questions or exit on Enter
- Implement toggleToActMode callback for mode switching
- Update keyboard handlers and UI rendering for new prompt types
- Add helpful hints for users (e.g., "just Enter to switch to Act mode")
2026-01-19 21:01:36 -08:00
abeatrix d6a1b338bc clean up 2026-01-19 19:18:13 -08:00
abeatrix 0091fb6ffb clean up messages 2026-01-19 16:38:47 -08:00
abeatrix de3e3fe34a clean up logs 2026-01-19 16:25:43 -08:00
abeatrix e136d75621 refactor: switch CLI to ES modules with Ink UI
Convert CLI build output from CommonJS (cli.cjs) to ES modules (cli.mjs) and refactor the UI layer to use React with Ink framework instead of the previous subscription-based state update pattern.

Changes include:
- Updated esbuild configuration to target ESM format
- Added stubOptionalModulesPlugin to handle react-devtools-core
- Updated shebang and module compatibility helpers for ES modules
- Replaced state subscriber pattern with React component-based UI
- Added react and related type dependencies
- Updated external module list to include UI dependencies (ink, ink-spinner, react)
- Enabled top-level-await support for ES modules
2026-01-19 16:18:24 -08:00
abeatrix 91127ffdb9 Update logger 2026-01-16 22:34:20 -08:00
abeatrix 11f76f0218 add cline sign in 2026-01-16 22:08:03 -08:00
abeatrix 74f1b9c75d **feat(cli): allow switching mode and specifying model for tasks**
Added `--switch` (`-s`) and `--model` (`-m`) options to the CLI.
Updated `runTask` signature and logic to set global state for mode (plan/act) and the corresponding API model ID.
This enables users to run tasks in different modes or with a specific model directly from the command line.
2026-01-16 18:46:55 -08:00
abeatrix 942c78119e working prototype
npm install:all
cd cli-ts
npm run link
clinedev auth
2026-01-16 18:36:47 -08:00
abeatrix 3130c3c96e replace console.log with Logger 2026-01-14 13:08:50 -08:00
abeatrix f0225bc20d fileeditprovider 2026-01-14 13:08:28 -08:00
abeatrix 1bf158245c prototype: Cline CLI with Typescript
TODO:
- remove usage of console.log across codebase and replace them with Logger
2026-01-13 16:55:36 -08:00
772 changed files with 50078 additions and 42409 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add .agents/skills as default skill directory (global and local)
+7
View File
@@ -0,0 +1,7 @@
---
"claude-dev": patch
---
Add endpoint configuration file support for on-premise deployments
Enterprise customers can now configure custom API endpoints by creating a `~/.cline/endpoints.json` file with custom URLs for `appBaseUrl`, `apiBaseUrl`, and `mcpBaseUrl`. When this file is present, Cline runs in on-premise mode with the custom endpoints.
-10
View File
@@ -1,10 +0,0 @@
---
"claude-dev": patch
---
Add comprehensive LLM evaluation framework with CI integration
- Smoke tests: 7 curated scenarios testing tools across providers (Claude, GPT-5, Gemini)
- Analysis framework: pass@k metrics for measuring reliability
- CI workflow: Parallel smoke tests on PRs with ~3min execution time
- E2E runner: cline-bench integration for real-world task evaluation (local only for now)
@@ -0,0 +1,7 @@
---
"claude-dev": patch
---
fix: prevent infinite retry loops when replace_in_file fails repeatedly
Add safeguards to prevent the LLM from getting stuck in infinite retry loops when `replace_in_file` operations fail repeatedly. The system now detects repeated failures and provides better guidance to break out of retry cycles.
@@ -0,0 +1,7 @@
---
"claude-dev": patch
---
fix: skip diff error UI handling during streaming to prevent flickering
Suppress diff view error notifications while content is actively streaming to prevent visual flickering and improve user experience. Error handling is deferred until streaming completes.
+7
View File
@@ -0,0 +1,7 @@
---
"claude-dev": patch
---
fix(extract-text): strip notebook outputs to reduce context size
Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing the amount of context sent to the LLM while preserving the essential code and markdown content.
+7
View File
@@ -0,0 +1,7 @@
---
"claude-dev": patch
---
fix: throttle diff view updates during streaming
Add throttling to diff view updates during content streaming to reduce UI flickering and improve performance. Updates are now batched at reasonable intervals instead of firing on every token received.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Disable PostHog telemetry, error tracking, and feature flags in self-hosted mode
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Remove deprecated zai-glm-4.6 model from Cerebras provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Make Sonnet 4.5 the default Amazon Bedrock model id
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Disable PostHog and build-time OpenTelemetry telemetry in self-hosted/on-premise mode. Enterprise customers running self-hosted deployments will no longer send any telemetry to Cline's collectors. Runtime environment OTEL and remote config OTEL remain available for enterprises to configure their own telemetry collection.
+5 -20
View File
@@ -147,29 +147,14 @@ When filling out the template:
### Create PR with gh CLI
**Use a temporary file for the PR body** to avoid shell escaping issues, newline problems, and other command-line flakiness:
1. Write the PR body to a temporary file:
```
/tmp/pr-body.md
```
2. Create the PR using the file:
```bash
gh pr create --title "PR_TITLE" --body-file /tmp/pr-body.md --base main
```
3. Clean up the temporary file:
```bash
rm /tmp/pr-body.md
```
For draft PRs:
```bash
gh pr create --title "PR_TITLE" --body-file /tmp/pr-body.md --base main --draft
gh pr create --title "PR_TITLE" --body "PR_BODY" --base main
```
**Why use a file?** Passing complex markdown with newlines, special characters, and checkboxes directly via `--body` is error-prone. The `--body-file` flag handles all content reliably.
Alternatively, create as draft if the user wants review before marking ready:
```bash
gh pr create --title "PR_TITLE" --body "PR_BODY" --base main --draft
```
## Post-Creation
+4 -4
View File
@@ -1,8 +1,8 @@
# CLI Development
The CLI lives in `cli/` and uses React Ink for terminal UI.
The CLI lives in `cli-ts/` and uses React Ink for terminal UI.
- If needed, look at `cli/src/constants/colors.ts` for re-used terminal colors, e.g. `COLORS.primaryBlue` highlight color (selections, spinners, success states).
- If needed, look at `cli-ts/src/constants/colors.ts` for re-used terminal colors, e.g. `COLORS.primaryBlue` highlight color (selections, spinners, success states).
- Never use `dimColor` with gray (e.g. `<Text color="gray" dimColor>`) - it's too hard to read. Use `color="gray"` for secondary text and normal foreground (no color) for primary text.
- When thinking about how to handle state or messages from core, look at webview for how it communicates with the vs code extension.
- When updating the webview, consider and suggest to the user to update the CLI TUI since we want to provide a similar experience to our terminal users as we do our vs code extension users.
@@ -11,7 +11,7 @@ The CLI lives in `cli/` and uses React Ink for terminal UI.
When adding a new API provider to the extension, you must also update the CLI:
1. **Update `cli/src/components/ModelPicker.tsx`**: Add the provider to the `providerModels` map so `getDefaultModelId()` returns the correct default model. Import the models and default ID from `@shared/api`:
1. **Update `cli-ts/src/components/ModelPicker.tsx`**: Add the provider to the `providerModels` map so `getDefaultModelId()` returns the correct default model. Import the models and default ID from `@shared/api`:
```typescript
import { newProviderDefaultModelId, newProviderModels } from "@/shared/api"
@@ -21,7 +21,7 @@ When adding a new API provider to the extension, you must also update the CLI:
}
```
2. **Use `applyProviderConfig()` for auth flows**: When implementing OAuth or other auth flows for the provider, use the shared utility at `cli/src/utils/provider-config.ts`:
2. **Use `applyProviderConfig()` for auth flows**: When implementing OAuth or other auth flows for the provider, use the shared utility at `cli-ts/src/utils/provider-config.ts`:
```typescript
import { applyProviderConfig } from "../utils/provider-config"
-11
View File
@@ -147,17 +147,6 @@ Required steps:
Common mistake: Adding only the return value without the `context.globalState.get()` call. This compiles but the value is always `undefined` on load.
Settings plumbing gotcha: if a key is user-toggleable from settings, wire both controller update paths:
- `src/core/controller/state/updateSettings.ts` for webview `updateSetting(...)`
- `src/core/controller/state/updateSettingsCli.ts` for CLI/ACP settings updates
Missing one path causes a toggle to appear to change in one surface while the backend state stays unchanged.
Webview toggle gotcha: settings changes must also round-trip back in state payloads.
- Add the field to `UpdateSettingsRequest` in `proto/cline/state.proto` (for webview update requests), then run `npm run protos`
- Include the key in `Controller.getStateToPostToWebview()` (`src/core/controller/index.ts`)
- Ensure `ExtensionState` and webview defaults include the key (`src/shared/ExtensionMessage.ts`, `webview-ui/src/context/ExtensionStateContext.tsx`)
If this round-trip wiring is missing, the backend value can update but the toggle in webview appears stuck or reverts.
## StateManager Cache vs Direct globalState Access
StateManager uses an in-memory cache populated during `StateManager.initialize(context)` in `common.ts`. For most state, use `controller.stateManager.setGlobalState()`/`getGlobalStateKey()`.
-49
View File
@@ -1,49 +0,0 @@
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
version = 1
name = "cline"
[setup]
script = '''
if [ ! -d "node_modules" ]; then
MAIN_WORKTREE="$(git worktree list | head -n1 | awk '{print $1}')"
ln -s "$MAIN_WORKTREE/node_modules" node_modules
ln -s "$MAIN_WORKTREE/webview-ui/node_modules" webview-ui/node_modules
fi
'''
[[actions]]
name = "VS Code"
icon = "run"
command = "chmod +x ./scripts/run-extension-host.sh && ./scripts/run-extension-host.sh production"
[[actions]]
name = "CLI"
icon = "run"
command = '''
npm run cli:build
npm run cli:run
'''
[[actions]]
name = "npm install"
icon = "tool"
command = '''
rm node_modules
rm webview-ui/node_modules
npm run install:all
'''
[[actions]]
name = "pull main"
icon = "tool"
command = '''
git fetch origin main
if ! git merge-base --is-ancestor main origin/main; then
echo "Local main has commits not on origin/main. Aborting..."
exit 1
fi
git update-ref refs/heads/main refs/remotes/origin/main
echo "main updated to $(git rev-parse --short main)"
'''
+7
View File
@@ -16,6 +16,13 @@
TELEMETRY_SERVICE_API_KEY=your-posthog-telemetry-api-key
ERROR_SERVICE_API_KEY=your-posthog-error-tracking-api-key
# ============================================================================
# TELEMETRY PROVIDER CONTROL
# ============================================================================
# Control which telemetry providers are active
POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: true)
# Set to false to disable Telemetry completely
# ============================================================================
# OPENTELEMETRY (Optional - for advanced telemetry)
# ============================================================================
+3 -2
View File
@@ -1,2 +1,3 @@
/.github/ @saoudrizwan @arafatkatze @maxpaulus43 @candieduniverse
/README.md @saoudrizwan @juanpflores
/docs/
/.github/ @saoudrizwan @garoth @sjf
/README.md @saoudrizwan @nickbaumann98
+173
View File
@@ -0,0 +1,173 @@
name: Claude Issue Triage
on:
issues:
types: [opened]
# Manual trigger for backfilling existing issues. Run from terminal:
# gh workflow run claude-issue-triage.yml -f issue_number=1234
# Or batch process:
# gh issue list --state open --limit 10 --json number --jq '.[].number' | while read num; do
# gh workflow run claude-issue-triage.yml -f issue_number=$num
# sleep 60
# done
workflow_dispatch:
inputs:
issue_number:
description: 'Issue number to triage'
required: true
type: string
jobs:
claude-issue-triage:
runs-on: ubuntu-latest
timeout-minutes: 120
# SECURITY: These permissions are intentionally restrictive.
# - contents: read -> Claude can read the codebase but CANNOT write/push any code
# - issues: write -> Claude can comment and add labels (the only write access needed)
# - pull-requests: read -> Claude can view PR context but CANNOT create PRs
# This ensures that even if a malicious user attempts prompt injection via issue content,
# Claude cannot modify repository code, create branches, or open PRs.
permissions:
contents: read
issues: write
pull-requests: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run Issue Response & Triage
id: triage
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
allowed_non_write_users: "*"
# Allow all tools - security is enforced by GitHub permissions above (contents: read, issues: write)
claude_args: --model claude-opus-4-5-20251101 --allowedTools "Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch"
prompt: |
You're a GitHub issue first responder for the open source Cline repository.
**Issue:** #${{ github.event.issue.number || inputs.issue_number }}
**Title:** ${{ github.event.issue.title || 'See issue details below' }}
**Author:** @${{ github.event.issue.user.login || 'See issue details below' }}
## Your job
Investigate this issue thoroughly, then post a single helpful comment that helps the user and gives maintainers the context they need.
## Investigation
Start by reading the full issue:
gh issue view ${{ github.event.issue.number || inputs.issue_number }}
### Search for duplicates and related issues
Search thoroughly for existing issues that match this one:
gh issue list --search "<keywords from the issue>" --state all --limit 30
gh issue list --search "<error messages>" --state all --limit 20
gh issue list --search "<affected feature/component>" --state all --limit 20
For each relevant issue you find, read it including its comments:
gh issue view <number> --comments
You're looking for:
- **Duplicates**: Issues describing the same problem. Link to them and explain why you think they're duplicates. If closed, check how they were resolved - the solution might apply here.
- **Related issues**: Similar problems or context that could help. Pull useful information from their comments (workarounds others found, debugging steps that helped, maintainer explanations). Link to them and explain the connection.
If there are closed issues with solutions, surface those solutions prominently - this might immediately solve the user's problem.
### Analyze recent changes (ALWAYS DO THIS)
Many issues are regressions from recent releases. **Always** check what changed recently:
gh release list --limit 10
gh pr list --state merged --limit 50 --json number,title,mergedAt,author,body
Look for PRs merged in the last few weeks that might correlate with the issue. If you find a likely connection:
gh pr view <number>
gh pr diff <number>
git log --since="1 month ago" --oneline -- <relevant paths>
git show <commit>
**Always include your findings in your comment:**
- If you find a regression, call it out explicitly: which PR/commit likely caused it, who authored it, what changed, and suggest a fix direction if you can see one.
- If you don't find anything related, still mention it: "I analyzed recent PRs and releases but didn't find any changes that seem related to this issue."
### Search the codebase
Find the relevant code:
- Use grep/find to locate code related to the issue
- Key areas: `src/api/` (providers/models), `src/core/prompts/` (tools/prompts), platform-specific code for VS Code vs JetBrains
### Find documentation
Cline docs are at **https://docs.cline.bot/** and built with Mintlify from the `docs/` directory.
The URL structure maps directly to the file structure:
- `docs/getting-started/selecting-your-model.mdx` → https://docs.cline.bot/getting-started/selecting-your-model
- `docs/troubleshooting.mdx` → https://docs.cline.bot/troubleshooting
- Headings become anchors: `## Which Model` → `#which-model`
Search the `docs/` directory to find relevant documentation, then construct URLs to link users to:
```bash
ls docs/
grep -r "keyword" docs/ --include="*.mdx" -l
```
### Identify subject matter experts
For issues that clearly need engineering attention:
git log --since="6 months ago" --format="%an" -- <relevant paths> | sort | uniq -c | sort -rn | head -5
Cross-reference with GitHub usernames. Include in your response (@mention, do NOT assign):
| SME | Reason |
|-----|--------|
| @username1 | Authored PR #X which modified this area |
| @username2 | Primary contributor to affected file |
## Weak model detection
Many issues are caused by users running small or non-frontier models that don't tool-call reliably. Signs include:
- Model failing to use tools correctly
- Nonsensical or malformed responses
- User is running a small/local model or older model version
If this looks like a weak model issue, kindly suggest they try reproducing with Claude Sonnet and report back if it persists. Link to https://docs.cline.bot/getting-started/selecting-your-model if helpful. Still label and triage normally.
## Your comment
Write a single comment as a helpful community member. Be conversational, not robotic. Include what's relevant:
- **Helpful response** - Answer their question, suggest a fix, provide a workaround. If you found solutions in related closed issues, surface those prominently.
- **Duplicates and related issues** - Link to any you found and explain why they're duplicates/related. Summarize useful context from their comments.
- **Regression analysis** - If this looks like a regression, explain what change likely caused it, link to the PR/commit, and tag the author.
- **Clarifying questions** - If you need more info, ask specific questions. Don't ask for things already provided.
- **SME table** - Include the table above if this needs engineering attention. Don't tag people for questions with obvious answers or weak-model issues.
- **Context for maintainers** - Relevant code paths, what you found. Keep it concise.
- **Docs links** - If there's relevant documentation, link to it naturally in your response as a recommendation (e.g., "For more details, check out [the Ollama setup guide](url)"). Do NOT add a "Sources" section at the end - integrate doc links into your response where they're helpful.
- **Possible Duplicates section** - ALWAYS include a "Possible Duplicates" section at the end of your comment listing issues that might be duplicates so maintainers can quickly close if appropriate. If none found, say "No obvious duplicates found."
## Labels
First, retrieve all available labels and read their descriptions to understand what each is for:
gh label list --json name,description --limit 100
Then apply the appropriate labels based on your analysis. Only use labels from the list above—do not create new labels.
gh issue edit ${{ github.event.issue.number || inputs.issue_number }} --add-label "label1,label2"
If your regression analysis found a likely culprit (a recent PR/commit that probably caused this issue), add the "Regression" label:
gh issue edit ${{ github.event.issue.number || inputs.issue_number }} --add-label "Regression"
IMPORTANT: After posting your comment, add the "Bot Responded" label to indicate this issue has received an automated response:
gh issue edit ${{ github.event.issue.number || inputs.issue_number }} --add-label "Bot Responded"
## Remember
- **This is a one-time automated response** - you will NOT see their reply or respond again. Never say things like "I can help you", "let me know", "once I have that info", or "I can give you more targeted help" - you won't be there to follow up. If you ask clarifying questions, frame them for the maintainers who will follow up, e.g., "If you can share X, that would help the maintainers diagnose this."
- Don't be formulaic. Respond to what the issue actually needs.
- Surface solutions from past issues - often the fastest path to helping.
- Connecting regressions to specific changes is extremely valuable.
- Link issues with #number so they're clickable.
+272
View File
@@ -0,0 +1,272 @@
name: Claude PR Review
on:
pull_request:
types: [opened, ready_for_review]
# Manual trigger for backfilling existing PRs. Run from terminal:
# gh workflow run claude-pr-review.yml -f pr_number=1234
# Or batch process open PRs:
# gh pr list --state open --limit 10 --json number --jq '.[].number' | while read num; do
# gh workflow run claude-pr-review.yml -f pr_number=$num
# sleep 60
# done
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to review'
required: true
type: string
jobs:
claude-pr-review:
# Runs on PR opened/ready_for_review (skips drafts) or manual trigger for backfilling
if: |
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 120
# SECURITY: These permissions are intentionally restrictive.
# - contents: read -> Claude can read the codebase but CANNOT write/push any code
# - pull-requests: write -> Claude can post reviews and inline suggestions
# - issues: read -> Claude can search for related issues
# NOTE: Even with pull-requests: write, Claude CANNOT merge PRs because branch protection
# requires 1 approval from a Code Owner. The GITHUB_TOKEN cannot bypass this.
permissions:
contents: read
pull-requests: write
issues: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get PR number
id: pr
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "number=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
else
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
fi
- name: Run PR Review
id: review
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
allowed_non_write_users: "*"
claude_args: --model claude-opus-4-5-20251101 --allowedTools "Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch"
prompt: |
You're a GitHub PR reviewer for the open source Cline repository. Your goal is to give the PR author helpful feedback and give maintainers the context they need to review efficiently.
PR: #${{ steps.pr.outputs.number }}
## Gather context
```bash
# Get full PR details
gh pr view ${{ steps.pr.outputs.number }} --json number,title,body,author,createdAt,updatedAt,isDraft,labels,commits,files,additions,deletions,changedFiles,baseRefName,headRefName,mergeable,reviewDecision
# Get the diff
gh pr diff ${{ steps.pr.outputs.number }}
# Check CI status
gh pr checks ${{ steps.pr.outputs.number }}
# Get existing review comments (to understand context and your previous feedback)
gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/comments --jq '.[] | {user: .user.login, body: .body, path: .path, created_at: .created_at}'
# Get conversation comments
gh pr view ${{ steps.pr.outputs.number }} --comments
```
If this is a re-review (workflow_dispatch event):
Read your previous comments carefully. Understand what you asked for before.
Check if new commits or comments address your previous feedback.
## Check contributing guidelines
Flag (but don't block) if:
- Missing changeset - For user-facing changes, check if there's a `.changeset/` file:
```bash
gh pr diff ${{ steps.pr.outputs.number }} --name-only | grep '.changeset/' || echo "No changeset found"
```
If missing, ask them to run `npm run changeset`
- Missing tests - New features should have tests
## Find related issues and PRs
Search thoroughly for context that might help with the review:
```bash
# Find related issues for context
gh issue list --search "<keywords from the PR>" --state all --limit 30
gh issue list --search "<error messages or feature names>" --state all --limit 20
# Find similar PRs for reference
gh pr list --search "<keywords>" --state all --limit 30
```
For each relevant issue or PR you find, read it including comments:
```bash
gh issue view <number> --comments
gh pr view <number> --comments
```
Look for:
- Open issues this PR might fix that weren't linked in the description
- Similar PRs that went through review - what feedback did they get? What patterns did they follow?
- Context from maintainer discussions that could inform your review
## Find subject matter experts
For files changed in this PR, find who knows the code best:
```bash
# Get files changed
gh pr diff ${{ steps.pr.outputs.number }} --name-only
# For each relevant path, find contributors
git log --since="6 months ago" --format="%an" -- <path> | sort | uniq -c | sort -rn | head -5
```
Cross-reference git authors with GitHub usernames. Include an SME table in your response:
| SME | Reason |
|-----|--------|
| @username1 | Authored PR #X which modified this area |
| @username2 | Primary contributor to affected file (15 commits in 6 months) |
| @username3 | Reviewed similar PR #Y with extensive feedback |
## Deep code review
This is the most important part. Don't just look for syntax issues - understand what the PR is trying to achieve and whether the implementation is the right approach.
Step 1: Understand the intent
Read the PR description and understand what the author is trying to accomplish. What problem are they solving? What feature are they adding?
Step 2: Form your own opinion first
Before analyzing their code, think about how YOU would implement this feature or fix. What files would you touch? What patterns would you follow? What edge cases would you handle?
Step 3: Compare approaches
Now look at their implementation. How does it compare to what you would have done?
- Is their approach better in some ways? Note what they did well.
- Is their approach missing something? Be specific about what and why.
- Are there edge cases they haven't considered?
- Does it follow the patterns established in similar parts of the codebase?
Step 4: Look at the bigger picture
- What other files or systems does this change interact with?
- Could this break anything else?
- Is there additional work needed beyond this PR to complete the feature?
- Does this fit well with the overall architecture?
Step 5: Find reference implementations
Look for similar changes in the codebase:
```bash
git log --oneline --all --grep="<relevant keywords>" | head -20
git log --oneline -- <similar files> | head -20
```
If this is adding a new API provider, look at how other providers are implemented.
If this is adding a new feature, look at how similar features were added.
Note where their implementation aligns with or diverges from established patterns.
Step 6: Standard code review checks
- DRY: Is there duplicated code that could be extracted?
- Error handling: Are errors handled appropriately?
- Security: Any injection risks, credential exposure, unsafe dependencies?
- Performance: Any obvious inefficiencies, memory leaks, N+1 patterns?
- Types: Is TypeScript used correctly? Any unsafe type assertions?
- Naming: Are variables and functions named clearly?
- Comments: Is complex logic explained? Are there outdated comments?
## Inline code suggestions
For specific code improvements, use GitHub's suggestion syntax via `gh api`.
This creates suggestions the author can commit with one click.
Single-line suggestion:
```bash
gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews \
-X POST \
-f commit_id="$(gh pr view ${{ steps.pr.outputs.number }} --json headRefOid -q .headRefOid)" \
-f event="COMMENT" \
-f body="" \
-F comments='[
{
"path": "src/example.ts",
"line": 42,
"body": "Consider simplifying:\n\n```suggestion\nconst result = items.filter(Boolean);\n```"
}
]'
```
Multi-line suggestion (replacing lines 40-45):
```bash
gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews \
-X POST \
-f commit_id="$(gh pr view ${{ steps.pr.outputs.number }} --json headRefOid -q .headRefOid)" \
-f event="COMMENT" \
-f body="" \
-F comments='[
{
"path": "src/example.ts",
"start_line": 40,
"line": 45,
"body": "This can be simplified:\n\n```suggestion\nconst simplified = doThing();\n```"
}
]'
```
Use inline suggestions for concrete improvements. Use regular comments for questions or broader feedback.
## Post your review
After your investigation, post a single helpful comment that helps the author and gives maintainers context.
Start with a warm thank you for their contribution. Be conversational, not robotic.
Include what's relevant:
- In-depth explanation of what the PR does - Be comprehensive. A maintainer should be able to read this section and fully understand the author's intent, why they made the changes, how they implemented it, and what files/systems are affected. Don't just summarize - explain.
- Related issues/PRs you found that provide useful context (link to them)
- Your review findings (issues to address, suggestions, etc.)
- Clear next steps for the author
Include a "For Maintainers" section with:
- Anything else useful to help the maintainer resolve this PR
- Related issues/PRs with context on why they're relevant
- Open issues this PR might fix that weren't linked in the description
- Your recommendation: merge as-is, needs changes, needs discussion, close, etc.
- SME table - who should review this and why
For the SME table:
| SME | Reason |
|-----|--------|
| @username | Primary contributor to affected files |
## Update labels
Add appropriate labels based on your analysis:
```bash
gh label list --json name,description --limit 100
gh pr edit ${{ steps.pr.outputs.number }} --add-label "label1,label2"
```
When done, add the reviewed label:
```bash
gh pr edit ${{ steps.pr.outputs.number }} --add-label "Bot Reviewed"
```
## Remember
- This is a one-time automated response - you will NOT see their reply or respond again. Never say things like "let me know if you have questions", "I can help you with", or "feel free to ask" - you won't be there to follow up. Frame any questions for the maintainers who will follow up.
- Be helpful and welcoming - Many contributors are new to the project
- Be specific - Point to exact lines and suggest fixes, don't give vague feedback
- Think deeply - Don't just surface-level review, understand the intent and evaluate the approach
- Use inline suggestions - Make it easy for authors to accept changes
- You're a first-pass reviewer - A human maintainer will do final approval
@@ -1,70 +0,0 @@
name: Smoke Tests
on:
push:
branches: [main]
paths:
- 'src/core/**'
- 'src/shared/**'
- 'proto/**'
- 'evals/**'
- '.github/workflows/cline-evals-regression.yml'
pull_request:
paths:
- 'src/core/**'
- 'src/shared/**'
- 'proto/**'
- 'evals/**'
- '.github/workflows/cline-evals-regression.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: smoke-tests-${{ github.ref }}
cancel-in-progress: true
jobs:
smoke-tests:
name: Smoke Tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build and install CLI
run: |
npm run protos
cd cli && npm install && npm run build && npm link
echo "$(npm config get prefix)/bin" >> $GITHUB_PATH
- name: Verify CLI
run: cline --version
- name: Run smoke tests
env:
CLINE_API_KEY: ${{ secrets.CLINE_API_KEY }}
run: |
cline auth -p cline -k "$CLINE_API_KEY" -m "anthropic/claude-sonnet-4.5"
npx tsx evals/smoke-tests/run-smoke-tests.ts --trials 1 --parallel
- name: Generate summary
if: always()
run: cat evals/smoke-tests/results/latest/summary.md >> $GITHUB_STEP_SUMMARY
- name: Upload results
uses: actions/upload-artifact@v4
if: always()
with:
name: smoke-test-results-${{ github.run_id }}
path: evals/smoke-tests/results/latest/
retention-days: 30
+312
View File
@@ -0,0 +1,312 @@
name: Cline PR Code Review
on:
pull_request:
types:
[opened, ready_for_review]
# Manual trigger for backfilling existing PRs. Run from terminal:
# gh workflow run cline-pr-review.yml -f pr_number=1234
# Or batch process open PRs:
# gh pr list --state open --limit 10 --json number --jq '.[].number' | while read num; do
# gh workflow run cline-pr-review.yml -f pr_number=$num
# sleep 60
# done
workflow_dispatch:
inputs:
pr_number:
description: "PR number to review"
required: true
type: string
concurrency:
group: pr-review-${{ github.event.pull_request.number || inputs.pr_number }}
cancel-in-progress: true
jobs:
cline-pr-review:
# Runs on PR opened/ready_for_review (skips drafts) or manual trigger for backfilling
if: |
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 60
# SECURITY: These permissions are intentionally restrictive.
# - contents: read -> cline can read the codebase but CANNOT write/push any code
# - pull-requests: write -> cline can post reviews and inline suggestions
# - issues: read -> cline can search for related issues
# NOTE: Even with pull-requests: write, cline CANNOT merge PRs because branch protection
# requires 1 approval from a Code Owner. The GITHUB_TOKEN cannot bypass this.
permissions:
contents: read
pull-requests: write
issues: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: "npm"
- name: Install and Verify Cline CLI
run: |
npx cline version # verify installation
- name: Configure Cline with Anthropic
run: |
npx cline auth --provider anthropic \
--apikey "${{ secrets.ANTHROPIC_API_KEY }}" \
--modelid claude-opus-4-5-20251101
- name: Get PR number
id: pr
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "number=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
else
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
fi
- name: Review PR with Cline
env:
PR_NUMBER: ${{ steps.pr.outputs.number }}
GITHUB_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
CLINE_COMMAND_PERMISSIONS: |
{
"allow": [
"gh pr diff *",
"gh pr view *",
"gh pr checks *",
"gh pr list *",
"gh label list *",
"gh issue list *",
"gh issue view *",
"git log *",
"gh pr comment ${{ steps.pr.outputs.number }} *",
"gh pr edit ${{ steps.pr.outputs.number }} *",
"gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/comments *",
"gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews *"
]
}
run: |
npx cline --yolo 'You'\''re a GitHub PR reviewer for the open source Cline repository. Your goal is to give the PR author helpful feedback and give maintainers the context they need to review efficiently.
PR: #'"${PR_NUMBER}"'
## Gather context
```bash
# Get full PR details
gh pr view '"${PR_NUMBER}"' --json number,title,body,author,createdAt,updatedAt,isDraft,labels,commits,files,additions,deletions,changedFiles,baseRefName,headRefName,mergeable,reviewDecision
# Get the diff
gh pr diff '"${PR_NUMBER}"'
# Check CI status
gh pr checks '"${PR_NUMBER}"'
# Get existing review comments (to understand context and your previous feedback)
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/comments --jq '\''.[] | {user: .user.login, body: .body, path: .path, created_at: .created_at}'\''
# Get conversation comments
gh pr view '"${PR_NUMBER}"' --comments
```
If this is a re-review (workflow_dispatch event):
Read your previous comments carefully. Understand what you asked for before.
Check if new commits or comments address your previous feedback.
## Check contributing guidelines
Flag (but don'\''t block) if:
- Missing changeset - For user-facing changes, check if there'\''s a `.changeset/` file:
```bash
gh pr diff '"${PR_NUMBER}"' --name-only | grep '\''.changeset/'\'' || echo '\''No changeset found'\''
```
If missing, ask them to run `npm run changeset`
- Missing tests - New features should have tests
## Find related issues and PRs
Search thoroughly for context that might help with the review:
```bash
# Find related issues for context
gh issue list --search '\''<keywords from the PR>'\'' --state all --limit 30
gh issue list --search '\''<error messages or feature names>'\'' --state all --limit 20
# Find similar PRs for reference
gh pr list --search '\''<keywords>'\'' --state all --limit 30
```
For each relevant issue or PR you find, read it including comments:
```bash
gh issue view <number> --comments
gh pr view <number> --comments
```
Look for:
- Open issues this PR might fix that weren'\''t linked in the description
- Similar PRs that went through review - what feedback did they get? What patterns did they follow?
- Context from maintainer discussions that could inform your review
## Find subject matter experts
For files changed in this PR, find who knows the code best:
```bash
# Get files changed
gh pr diff '"${PR_NUMBER}"' --name-only
# For each relevant path, find contributors
git log --since='\''6 months ago'\'' --format='\''%an'\'' -- <path> | sort | uniq -c | sort -rn | head -5
```
Cross-reference git authors with GitHub usernames. Include an SME table in your response:
| SME | Reason |
|-----|--------|
| @username1 | Authored PR #X which modified this area |
| @username2 | Primary contributor to affected file (15 commits in 6 months) |
| @username3 | Reviewed similar PR #Y with extensive feedback |
## Bash command usage
Don'\''t use operators like `|`, `&&`, or `;` - run each command separately and analyze the output.
When referencing command outputs, quote them properly to avoid formatting issues.
## Deep code review
This is the most important part. Don'\''t just look for syntax issues - understand what the PR is trying to achieve and whether the implementation is the right approach.
Step 1: Understand the intent
Read the PR description and understand what the author is trying to accomplish. What problem are they solving? What feature are they adding?
Step 2: Form your own opinion first
Before analyzing their code, think about how YOU would implement this feature or fix. What files would you touch? What patterns would you follow? What edge cases would you handle?
Step 3: Compare approaches
Now look at their implementation. How does it compare to what you would have done?
- Is their approach better in some ways? Note what they did well.
- Is their approach missing something? Be specific about what and why.
- Are there edge cases they haven'\''t considered?
- Does it follow the patterns established in similar parts of the codebase?
Step 4: Look at the bigger picture
- What other files or systems does this change interact with?
- Could this break anything else?
- Is there additional work needed beyond this PR to complete the feature?
- Does this fit well with the overall architecture?
Step 5: Find reference implementations
Look for similar changes in the codebase:
```bash
git log --oneline --all --grep='\''<relevant keywords>'\'' | head -20
git log --oneline -- <similar files> | head -20
```
If this is adding a new API provider, look at how other providers are implemented.
If this is adding a new feature, look at how similar features were added.
Note where their implementation aligns with or diverges from established patterns.
Step 6: Standard code review checks
- DRY: Is there duplicated code that could be extracted?
- Error handling: Are errors handled appropriately?
- Security: Any injection risks, credential exposure, unsafe dependencies?
- Performance: Any obvious inefficiencies, memory leaks, N+1 patterns?
- Types: Is TypeScript used correctly? Any unsafe type assertions?
- Naming: Are variables and functions named clearly?
- Comments: Is complex logic explained? Are there outdated comments?
## Inline code suggestions
For specific code improvements, use GitHub'\''s suggestion syntax via `gh api`.
This creates suggestions the author can commit with one click.
Single-line suggestion:
```bash
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/reviews \
-X POST \
-f commit_id="$(gh pr view '"${PR_NUMBER}"' --json headRefOid -q .headRefOid)" \
-f event='\''COMMENT'\'' \
-f body='\'''\'' \
-F comments='\''[
{
"path": "src/example.ts",
"line": 42,
"body": "Consider simplifying:\n\n```suggestion\nconst result = items.filter(Boolean);\n```"
}
]'\''
```
Multi-line suggestion (replacing lines 40-45):
```bash
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/reviews \
-X POST \
-f commit_id="$(gh pr view '"${PR_NUMBER}"' --json headRefOid -q .headRefOid)" \
-f event='\''COMMENT'\'' \
-f body='\'''\'' \
-F comments='\''[
{
"path": "src/example.ts",
"start_line": 40,
"line": 45,
"body": "This can be simplified:\n\n```suggestion\nconst simplified = doThing();\n```"
}
]'\''
```
Use inline suggestions for concrete improvements. Use regular comments for questions or broader feedback.
## Post your review
After your investigation, post a single helpful comment that helps the author and gives maintainers context.
Start with a warm thank you for their contribution. Be conversational, not robotic.
Include what'\''s relevant:
- In-depth explanation of what the PR does - Be comprehensive. A maintainer should be able to read this section and fully understand the author'\''s intent, why they made the changes, how they implemented it, and what files/systems are affected. Don'\''t just summarize - explain.
- Related issues/PRs you found that provide useful context (link to them)
- Your review findings (issues to address, suggestions, etc.)
- Clear next steps for the author
Include a '\''For Maintainers'\'' section with:
- Anything else useful to help the maintainer resolve this PR
- Related issues/PRs with context on why they'\''re relevant
- Open issues this PR might fix that weren'\''t linked in the description
- Your recommendation: merge as-is, needs changes, needs discussion, close, etc.
- SME table - who should review this and why
For the SME table:
| SME | Reason |
|-----|--------|
| @username | Primary contributor to affected files |
## Update labels
Add appropriate labels based on your analysis:
```bash
gh label list --json name,description --limit 100
gh pr edit '"${PR_NUMBER}"' --add-label '\''label1,label2'\''
```
When done, add the reviewed label:
```bash
gh pr edit '"${PR_NUMBER}"' --add-label '\''Bot Reviewed'\''
```
## Remember
- This is a one-time automated response - you will NOT see their reply or respond again. Never say things like '\''let me know if you have questions'\'', '\''I can help you with'\'', or '\''feel free to ask'\'' - you won'\''t be there to follow up. Frame any questions for the maintainers who will follow up.
- Be helpful and welcoming - Many contributors are new to the project
- Be specific - Point to exact lines and suggest fixes, don'\''t give vague feedback
- Think deeply - Don'\''t just surface-level review, understand the intent and evaluate the approach
- Use inline suggestions - Make it easy for authors to accept changes
- You'\''re a first-pass reviewer - A human maintainer will do final approval'
+2 -5
View File
@@ -80,16 +80,13 @@ jobs:
playwright-browsers-${{ runner.os }}-
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Install vsce
run: npm install -g @vscode/vsce
- name: Install xvfb on Linux
if: matrix.runner == 'ubuntu'
run: sudo apt-get update && sudo apt-get install -y xvfb
+42 -23
View File
@@ -1,7 +1,7 @@
name: Publish NPM Release
on:
workflow_call:
workflow_dispatch:
inputs:
confirm_publish:
description: 'Type "publish" to confirm you want to publish to NPM'
@@ -9,10 +9,9 @@ on:
type: string
permissions:
contents: write # Required for pushing tags
id-token: write # Required for npm trusted publishing (OIDC)
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
contents: read
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
jobs:
test:
@@ -21,7 +20,7 @@ jobs:
publish-npm-release:
needs: test
name: Publish Cline CLI to NPM
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main' && inputs.confirm_publish == 'publish'
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main' && github.event.inputs.confirm_publish == 'publish'
runs-on: ubuntu-latest
steps:
@@ -31,11 +30,32 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Install root dependencies and CLI dependencies
run: npm ci --include=optional # this will also install cli deps because "cli" in included in root package.json workspaces field
# Cache root dependencies
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# Cache cli-ts dependencies
- name: Cache cli-ts dependencies
uses: actions/cache@v4
id: cli-ts-cache
with:
path: cli-ts/node_modules
key: ${{ runner.os }}-npm-cli-ts-${{ hashFiles('cli-ts/package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci --include=optional
- name: Install cli-ts dependencies
if: steps.cli-ts-cache.outputs.cache-hit != 'true'
run: cd cli-ts && npm ci
- name: Generate Protos
run: npm run protos
@@ -43,12 +63,15 @@ jobs:
- name: Read release version
id: version
run: |
# Read version from cli/package.json
VERSION=$(node -p "require('./cli/package.json').version")
# Read version from cli-ts/package.json
VERSION=$(node -p "require('./cli-ts/package.json').version")
echo "Release version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Build standalone NPM package
- name: Clean previous builds
run: rm -rf dist-standalone
- name: Build and package CLI
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
@@ -59,6 +82,7 @@ jobs:
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
POSTHOG_TELEMETRY_ENABLED: "true"
run: node scripts/package-npm.mjs
- name: Verify build output
@@ -66,25 +90,20 @@ jobs:
echo "Checking dist-standalone directory..."
ls -la dist-standalone/
echo "Verifying CLI binaries..."
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
echo "Checking dist-standalone/dist..."
ls -la dist-standalone/dist/
echo "Checking package.json in dist-standalone..."
cat dist-standalone/package.json | grep version
cat dist-standalone/package.json
- name: Publish to NPM with latest tag
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_RELEASE_TOKEN }}
run: |
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'latest'..."
cd dist-standalone
npm publish --tag latest --access public
- name: Tag release
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag "v${{ steps.version.outputs.version }}-cli"
git push origin "v${{ steps.version.outputs.version }}-cli"
- name: Summary
run: |
echo "✅ Successfully published cline@${{ steps.version.outputs.version }} to NPM with tag 'latest'"
+48 -30
View File
@@ -1,19 +1,14 @@
name: Publish NPM Nightly
on:
workflow_call:
inputs:
force_publish:
description: "Force publish even if there are no commits in the last 24 hours"
required: false
type: boolean
default: false
schedule:
- cron: '0 12 * * *' # 4 AM PST (UTC-8) = 12 UTC
workflow_dispatch:
permissions:
contents: read
id-token: write # Required for npm trusted publishing (OIDC)
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
jobs:
test:
@@ -32,12 +27,6 @@ jobs:
- name: Check for recent commits
id: check_commits
run: |
if [ "${{ inputs.force_publish }}" = "true" ]; then
echo "force_publish enabled, proceeding with publish"
echo "skip=false" >> $GITHUB_OUTPUT
exit 0
fi
if [ $(git rev-list --count HEAD --since="24 hours ago") -eq 0 ]; then
echo "No commits in last 24 hours, skipping publish"
echo "skip=true" >> $GITHUB_OUTPUT
@@ -50,12 +39,34 @@ jobs:
if: steps.check_commits.outputs.skip != 'true'
uses: actions/setup-node@v4
with:
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Install root dependencies and CLI dependencies
# Cache root dependencies
- name: Cache root dependencies
if: steps.check_commits.outputs.skip != 'true'
run: npm ci --include=optional # this will also install cli deps because "cli" in included in root package.json workspaces field
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# Cache cli-ts dependencies
- name: Cache cli-ts dependencies
if: steps.check_commits.outputs.skip != 'true'
uses: actions/cache@v4
id: cli-ts-cache
with:
path: cli-ts/node_modules
key: ${{ runner.os }}-npm-cli-ts-${{ hashFiles('cli-ts/package-lock.json') }}
- name: Install root dependencies
if: steps.check_commits.outputs.skip != 'true' && steps.root-cache.outputs.cache-hit != 'true'
run: npm ci --include=optional
- name: Install cli-ts dependencies
if: steps.check_commits.outputs.skip != 'true' && steps.cli-ts-cache.outputs.cache-hit != 'true'
run: cd cli-ts && npm ci
- name: Generate Protos
if: steps.check_commits.outputs.skip != 'true'
@@ -65,32 +76,36 @@ jobs:
if: steps.check_commits.outputs.skip != 'true'
id: version
run: |
# Read base version from cli/package.json (e.g., "2.0.0")
BASE_VERSION=$(node -p "require('./cli/package.json').version")
# Read base version from cli-ts/package.json (e.g., "2.0.0")
BASE_VERSION=$(node -p "require('./cli-ts/package.json').version")
# Generate timestamp (Unix epoch seconds)
TIMESTAMP=$(date +%s)
# Create unique nightly version: 1.0.9-nightly.1736365200
# Create unique nightly version: 2.0.0-nightly.1736365200
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
echo "Base version: $BASE_VERSION"
echo "Generated nightly version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Update cli/package.json with nightly version
- name: Update cli-ts/package.json with nightly version
if: steps.check_commits.outputs.skip != 'true'
run: |
# Update version with timestamp-based nightly version
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('cli/package.json', 'utf8'));
const pkg = JSON.parse(fs.readFileSync('cli-ts/package.json', 'utf8'));
pkg.version = '${{ steps.version.outputs.version }}';
fs.writeFileSync('cli/package.json', JSON.stringify(pkg, null, '\t'));
fs.writeFileSync('cli-ts/package.json', JSON.stringify(pkg, null, '\t'));
"
echo "Using version ${{ steps.version.outputs.version }} for build"
cat cli/package.json | grep '"version"'
cat cli-ts/package.json | grep '"version"'
- name: Clean previous builds
if: steps.check_commits.outputs.skip != 'true'
run: rm -rf dist-standalone
- name: Build and package CLI
if: steps.check_commits.outputs.skip != 'true'
@@ -104,6 +119,7 @@ jobs:
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
POSTHOG_TELEMETRY_ENABLED: "true"
run: node scripts/package-npm.mjs
- name: Verify build output
@@ -112,14 +128,16 @@ jobs:
echo "Checking dist-standalone directory..."
ls -la dist-standalone/
echo "Verifying CLI binaries..."
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
echo "Checking dist-standalone/dist..."
ls -la dist-standalone/dist/
echo "Checking package.json in dist-standalone..."
cat dist-standalone/package.json | grep version
cat dist-standalone/package.json
- name: Publish to NPM with nightly tag
if: steps.check_commits.outputs.skip != 'true'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_RELEASE_TOKEN }}
run: |
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'nightly'..."
cd dist-standalone
-215
View File
@@ -1,215 +0,0 @@
# Build and Pack CLI
#
# Builds a CLI tarball from any branch/commit and publishes it as a GitHub Release.
# Requires write access to the repository (maintainers/collaborators only).
#
# Security: Split into two jobs to isolate untrusted build code from write tokens.
# The build job runs arbitrary ref code with zero permissions. The release job
# only runs trusted GitHub Actions with write scope.
#
# Usage (helper script, auto-detects current branch):
# ./scripts/build-cli-artifact.sh
# ./scripts/build-cli-artifact.sh feature/my-changes
# ./scripts/build-cli-artifact.sh feature/my-changes 1234 # comments on PR
#
# Usage (gh CLI directly):
# gh workflow run pack-cli.yml -f ref=main
# gh workflow run pack-cli.yml -f ref=abc123 -f pr_number=1234
#
# Install the built CLI (no auth required):
# npm install -g https://github.com/cline/cline/releases/download/cli-build-<sha>/cline-<ver>.tgz
#
# Find releases:
# gh release list --limit 10
name: Build and Pack CLI
permissions:
contents: read
on:
workflow_dispatch:
inputs:
ref:
description: 'Branch, tag, or commit SHA to build (leave empty for default branch)'
required: false
type: string
pr_number:
description: 'PR number to comment on with install instructions (optional)'
required: false
type: number
jobs:
# ── Build job: runs untrusted ref code with ZERO permissions ──
build:
name: Build CLI
runs-on: ubuntu-latest
permissions: {}
outputs:
commit_sha: ${{ steps.commit.outputs.sha }}
tarball: ${{ steps.pack.outputs.tarball }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
- name: Get commit SHA
id: commit
run: |
COMMIT_SHA=$(git rev-parse --short HEAD)
echo "sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "Building from commit: $COMMIT_SHA"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
- name: Install dependencies
run: npm ci --include=optional
- name: Generate Protos
run: npm run protos
- name: Build standalone package
run: node scripts/package-npm.mjs
- name: Create Tarball
id: pack
run: |
cd dist-standalone
TARBALL=$(npm pack)
echo "tarball=$TARBALL" >> $GITHUB_OUTPUT
echo "Created tarball: $TARBALL"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: cli-tarball
path: dist-standalone/*.tgz
# ── Release job: only trusted Actions code, with write permissions ──
release:
name: Release CLI
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: cli-tarball
path: dist-standalone
- name: Create GitHub Release
id: create_release
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
const commit = '${{ needs.build.outputs.commit_sha }}';
const tarball = '${{ needs.build.outputs.tarball }}';
// Delete existing release/tag if re-running for the same commit
const tagName = `cli-build-${commit}`;
try {
const existing = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: tagName
});
await github.rest.repos.deleteRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: existing.data.id
});
await github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `tags/${tagName}`
});
core.info(`Deleted existing release for ${tagName}`);
} catch (e) {
// Release doesn't exist yet, that's fine
}
// Create a release
const release = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tagName,
name: `CLI Build (${commit})`,
body: `Automated CLI build from commit ${commit}\n\nInstall with:\n\`\`\`bash\nnpm install -g https://github.com/${context.repo.owner}/${context.repo.repo}/releases/download/${tagName}/${tarball}\n\`\`\``,
draft: false,
prerelease: true
});
// Upload the tarball as a release asset
const tarballPath = path.join('dist-standalone', tarball);
const tarballData = fs.readFileSync(tarballPath);
await github.rest.repos.uploadReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: release.data.id,
name: tarball,
data: tarballData
});
const downloadUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/releases/download/${tagName}/${tarball}`;
core.setOutput('release_url', release.data.html_url);
core.setOutput('download_url', downloadUrl);
- name: Comment on PR with download instructions
if: inputs.pr_number != ''
uses: actions/github-script@v7
with:
script: |
const commit = '${{ needs.build.outputs.commit_sha }}';
const releaseUrl = '${{ steps.create_release.outputs.release_url }}';
const downloadUrl = '${{ steps.create_release.outputs.download_url }}';
const prNumber = ${{ inputs.pr_number || 0 }};
if (!prNumber) return;
const comment = `## 📦 CLI Build Ready
A CLI build has been created for commit \`${commit}\`.
### Install Directly from URL (No Authentication Required!)
\`\`\`bash
npm install -g ${downloadUrl}
\`\`\`
### Alternative: Download and Install
\`\`\`bash
curl -L ${downloadUrl} -o cline.tgz
npm install -g ./cline.tgz
\`\`\`
📦 [View Release](${releaseUrl})
`;
await github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
- name: Summary
run: |
echo "✅ CLI build complete!"
echo ""
echo "📦 Release: ${{ steps.create_release.outputs.release_url }}"
echo "🔗 Download URL: ${{ steps.create_release.outputs.download_url }}"
echo ""
echo "Install from anywhere (no authentication required):"
echo " npm install -g ${{ steps.create_release.outputs.download_url }}"
@@ -1,53 +0,0 @@
name: Publish CLI (Trusted)
on:
schedule:
- cron: "0 12 * * *" # 4 AM PST (UTC-8) = 12 UTC
workflow_dispatch:
inputs:
publish_target:
description: "Which publish flow to run"
required: true
default: "main"
type: choice
options:
- main
- nightly
confirm_publish:
description: 'Required when publish_target=main. Type "publish" to confirm release publish.'
required: false
type: string
force_nightly_publish:
description: "Force nightly publish even with no commits in last 24h"
required: false
type: boolean
default: false
permissions:
id-token: write # Required for npm trusted publishing (OIDC)
contents: write # Required because npm-main creates/pushes git tags
checks: write # Required by nested reusable test workflow
pull-requests: write # Required by nested reusable test workflow
jobs:
publish-main:
if: |
github.repository == 'cline/cline' && (
github.event_name == 'workflow_dispatch' &&
github.event.inputs.publish_target == 'main' &&
github.event.inputs.confirm_publish == 'publish' &&
!endsWith(github.actor, '[bot]')
)
uses: ./.github/workflows/npm-main.yaml
with:
confirm_publish: ${{ github.event.inputs.confirm_publish }}
publish-nightly:
if: |
github.repository == 'cline/cline' && (
github.event_name == 'schedule' ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.publish_target == 'nightly')
)
uses: ./.github/workflows/npm-nightly.yaml
with:
force_publish: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_nightly_publish == 'true' }}
+18
View File
@@ -38,10 +38,28 @@ jobs:
with:
node-version: "lts/*"
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci --include=optional
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci --include=optional
- name: Install Publishing Tools
+18
View File
@@ -44,10 +44,28 @@ jobs:
with:
node-version: "lts/*"
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm install --include=optional
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm install --include=optional
- name: Install Publishing Tools
+63 -25
View File
@@ -28,17 +28,27 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
package-lock.json
webview-ui/package-lock.json
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Run Quality Checks (Parallel)
@@ -63,17 +73,27 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
package-lock.json
webview-ui/package-lock.json
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Set up NPM on Windows
@@ -115,11 +135,6 @@ jobs:
cd webview-ui
npm run test:coverage
- name: CLI Tests
id: cli_tests
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
run: cd cli && npm run test:run
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
# Only upload artifacts on Linux - We only need coverage from one OS
@@ -141,18 +156,35 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
package-lock.json
webview-ui/package-lock.json
testing-platform/package-lock.json
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
# Cache testing-platform dependencies
- name: Cache testing-platform dependencies
uses: actions/cache@v4
id: testing-platform-cache
with:
path: testing-platform/node_modules
key: ${{ runner.os }}-npm-testing-platform-${{ hashFiles('testing-platform/package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Download ripgrep binaries
@@ -162,11 +194,17 @@ jobs:
run: npm run compile-standalone
- name: Install testing platform dependencies
if: steps.testing-platform-cache.outputs.cache-hit != 'true'
run: cd testing-platform && npm ci
- name: Running testing platform integration spec tests
continue-on-error: true
timeout-minutes: 7
run: npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
# Temporarily wrapping the test command to always return a neutral exit code.
# This prevents the job from showing as failed and avoids distracting developers
# until the integration tests are ready to be enforced.
run: |
npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
-4
View File
@@ -38,7 +38,6 @@ coverage-unit
src/generated/
src/shared/proto/
webview-ui/src/services/grpc-client.ts
*.tsbuildinfo
# E2E Tests
test-results
@@ -48,6 +47,3 @@ test-results
.secrets
*.tsbuildinfo
# Smoke test results (generated)
evals/smoke-tests/results/
-3
View File
@@ -1,3 +0,0 @@
[submodule "evals/cline-bench"]
path = evals/cline-bench
url = https://github.com/cline/cline-bench.git
+2 -117
View File
@@ -1,129 +1,14 @@
# Changelog
## [3.60.0]
- Fixes for Minimax model family
## [3.59.0]
- Added Minimax 2.5 Free Promo
- Fixed Response chaining for OpenAI's Responses API
## [3.58.0]
### Added
- Subagent: replace legacy subagents with the native `use_subagents` tool
- Bundle `endpoints.json` support so packaged distributions can ship required endpoints out-of-the-box
- Amazon Bedrock: support parallel tool calling
- New "double-check completion" experimental feature to verify work before marking tasks complete
- CLI: new task controls/flags including custom `--thinking` token budget and `--max-consecutive-mistakes` for yolo runs
- Remote config: new UI/options (including connection/test buttons) and support for syncing deletion of remotely configured MCP servers
- Vertex / Claude Code: add 1M context model options for Claude Opus 4.6
- ZAI/GLM: add GLM-5
### Fixed
- CLI: handle stdin redirection correctly in CI/headless environments
- CLI: preserve OAuth callback paths during auth redirects
- VS Code Web: generate auth callback URLs via `vscode.env.asExternalUri` (OAuth callback reliability)
- Terminal: surface command exit codes in results and improve long-running `execute_command` timeout behavior
- UI: add loading indicator and fix `api_req_started` rendering
- Task streaming: prevent duplicate streamed text rows after completion
- API: preserve selected Vercel model when model metadata is missing
- Telemetry: route PostHog networking through proxy-aware shared fetch and ensure telemetry flushes on shutdown
- CI: increase Windows E2E test timeout to reduce flakiness
### Changed
- Settings/model UX: move "reasoning effort" into model configuration and expose it in settings
- CLI provider selection: limit provider list to those remotely configured
- UI: consolidate ViewHeader component/styling across views
- Tools: add auto-approval support for `attempt_completion` commands
- Remotely configured MCP server schema now supports custom headers
## [3.57.1]
### Fixed
- Fixed Opus 4.6 for bedrock provider
## [3.57.0]
### Added
- Cline CLI 2.0 now available. Install with `npm install -g cline`
- Anthopic Opus 4.6
- Minimax-2.1 and Kimi-k2.5 now available for free for a limited time promo
- Codex-5.3 through ChatGPT subscription
### Fixed
- Fix read file tool to support reading large files
- Fix decimal input crash in OpenAI Compatible price fields (#8129)
- Fix build complete handlers when updating the api config
- Fixed missing provider from list
- Fixed Favorite Icon / Star from getting clipped in the task history view
### Changed
- Make skills always enabled and remove feature toggle setting
## [3.56.0]
### Added
- __CLI authentication:__ Added Vercel AI Gateway and Cline API key provider support for headless CI/automation workflows
- __New model:__ Added Kimi-K2.5 model to Moonshot provider (262K context, image support, prompt caching)
- __Prompt variant:__ Added Trinity Large prompt variant for improved tool-calling support
- __OpenTelemetry:__ Added support for custom headers on metrics and logs endpoints
- __Social links:__ Added community icons (X, Discord, GitHub, Reddit, LinkedIn) to the What's New modal
### Fixed
- __LiteLLM:__ Fixed thinking configuration not appearing for reasoning-capable models
- __OpenTelemetry:__ Fixed endpoint path handling (no longer incorrectly appends `/v1/logs` or `/v1/metrics`) and ensured logs are sent regardless of VSCode telemetry settings
- __CLI auth:__ Fixed `cline auth` displaying incorrect provider information after configuration
### Changed
- __Hooks:__ Hook scripts now run from the workspace repository root instead of filesystem root
- __Default settings:__ Enabled multi-root workspaces, parallel tool calling, and skills by default; disabled strict plan mode by default
- __Settings UI:__ Refreshed feature settings section with collapsible design
## [3.55.0]
- Add new model: Arcee Trinity Large Preview
- Add new model: Moonshot Kimi K2.5
- Add MCP prompts support - prompts from connected MCP servers now appear in slash command autocomplete as `/mcp:<server>:<prompt>`
## [3.54.0]
### Added
- Native tool calls support for Ollama provider
- Sonnet 4.5 is now the default Amazon Bedrock model id
### Fixed
- Prevent infinite retry loops when replace_in_file fails repeatedly. The system now detects repeated failures and provides better guidance to break out of retry cycles.
- Skip diff error UI handling during streaming to prevent flickering. Error handling is deferred until streaming completes.
- Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing context size sent to the LLM.
- Throttle diff view updates during streaming to reduce UI flickering and improve performance.
### Changed
- Removed Mistral's Devstral-2512 free from the free models list
- Removed deprecated zai-glm-4.6 model from Cerebras provider
## [3.53.1]
### Fixed
- Bug in responses API
- Bug in responses API
## [3.53.0]
### Fixed
- Removed grok model from free tier
- Removed grok model from free tier
## [3.52.0]
+1 -1
View File
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2026 Cline Bot Inc.
Copyright 2025 Cline Bot Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+1 -1
View File
@@ -148,4 +148,4 @@ Get the same Cline experience with enterprise-grade controls: SSO (SAML/OIDC), g
## License
[Apache 2.0 © 2026 Cline Bot Inc.](./LICENSE)
[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE)
-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.317 4.15557C18.7873 3.45369 17.147 2.93658 15.4319 2.6404C15.4007 2.63469 15.3695 2.64897 15.3534 2.67754C15.1424 3.05276 14.9087 3.54225 14.7451 3.927C12.9004 3.65083 11.0652 3.65083 9.25832 3.927C9.09465 3.5337 8.85248 3.05276 8.64057 2.67754C8.62449 2.64992 8.59328 2.63564 8.56205 2.6404C6.84791 2.93563 5.20756 3.45275 3.67693 4.15557C3.66368 4.16129 3.65233 4.17082 3.64479 4.18319C0.533392 8.83155 -0.31895 13.3657 0.0991801 17.8436C0.101072 17.8655 0.11337 17.8864 0.130398 17.8997C2.18321 19.4073 4.17171 20.3225 6.12328 20.9291C6.15451 20.9386 6.18761 20.9272 6.20748 20.9015C6.66913 20.2711 7.08064 19.6063 7.43348 18.9073C7.4543 18.8664 7.43442 18.8178 7.39186 18.8016C6.73913 18.554 6.1176 18.2521 5.51973 17.9093C5.47244 17.8816 5.46865 17.814 5.51216 17.7816C5.63797 17.6873 5.76382 17.5893 5.88396 17.4902C5.90569 17.4721 5.93598 17.4683 5.96153 17.4797C9.88928 19.273 14.1415 19.273 18.023 17.4797C18.0485 17.4674 18.0788 17.4712 18.1015 17.4893C18.2216 17.5883 18.3475 17.6873 18.4742 17.7816C18.5177 17.814 18.5149 17.8816 18.4676 17.9093C17.8697 18.2588 17.2482 18.554 16.5945 18.8006C16.552 18.8168 16.533 18.8664 16.5538 18.9073C16.9143 19.6054 17.3258 20.2701 17.7789 20.9005C17.7978 20.9272 17.8319 20.9386 17.8631 20.9291C19.8241 20.3225 21.8126 19.4073 23.8654 17.8997C23.8834 17.8864 23.8948 17.8664 23.8967 17.8445C24.3971 12.6676 23.0585 8.17064 20.3482 4.18414C20.3416 4.17082 20.3303 4.16129 20.317 4.15557ZM8.02002 15.117C6.8375 15.117 5.86313 14.0313 5.86313 12.6981C5.86313 11.3648 6.8186 10.2791 8.02002 10.2791C9.23087 10.2791 10.1958 11.3743 10.1769 12.6981C10.1769 14.0313 9.22141 15.117 8.02002 15.117ZM15.9947 15.117C14.8123 15.117 13.8379 14.0313 13.8379 12.6981C13.8379 11.3648 14.7933 10.2791 15.9947 10.2791C17.2056 10.2791 18.1705 11.3743 18.1516 12.6981C18.1516 14.0313 17.2056 15.117 15.9947 15.117Z" fill="#FAFAFA"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg viewBox="0 0 24 24" fill="black" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C6.477 2 2 6.477 2 12C2 16.418 4.865 20.166 8.84 21.49C9.34 21.58 9.52 21.27 9.52 21C9.52 20.77 9.51 20.14 9.51 19.31C6.73 19.91 6.14 17.97 6.14 17.97C5.68 16.81 5.03 16.5 5.03 16.5C4.12 15.88 5.1 15.9 5.1 15.9C6.1 15.97 6.63 16.93 6.63 16.93C7.5 18.45 8.97 18 9.54 17.76C9.63 17.11 9.89 16.67 10.17 16.42C7.95 16.17 5.62 15.31 5.62 11.5C5.62 10.39 6 9.5 6.65 8.79C6.55 8.54 6.2 7.5 6.75 6.15C6.75 6.15 7.59 5.88 9.5 7.17C10.29 6.95 11.15 6.84 12 6.84C12.85 6.84 13.71 6.95 14.5 7.17C16.41 5.88 17.25 6.15 17.25 6.15C17.8 7.5 17.45 8.54 17.35 8.79C18 9.5 18.38 10.39 18.38 11.5C18.38 15.32 16.04 16.16 13.81 16.41C14.17 16.72 14.5 17.33 14.5 18.26C14.5 19.6 14.49 20.68 14.49 21C14.49 21.27 14.67 21.59 15.17 21.49C19.14 20.16 22 16.42 22 12C22 6.477 17.523 2 12 2Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 902 B

-10
View File
@@ -1,10 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2001_1428)">
<path d="M22.2234 0H1.77187C0.792187 0 0 0.773438 0 1.72969V22.2656C0 23.2219 0.792187 24 1.77187 24H22.2234C23.2031 24 24 23.2219 24 22.2703V1.72969C24 0.773438 23.2031 0 22.2234 0ZM7.12031 20.4516H3.55781V8.99531H7.12031V20.4516ZM5.33906 7.43438C4.19531 7.43438 3.27188 6.51094 3.27188 5.37187C3.27188 4.23281 4.19531 3.30937 5.33906 3.30937C6.47813 3.30937 7.40156 4.23281 7.40156 5.37187C7.40156 6.50625 6.47813 7.43438 5.33906 7.43438ZM20.4516 20.4516H16.8937V14.8828C16.8937 13.5562 16.8703 11.8453 15.0422 11.8453C13.1906 11.8453 12.9094 13.2937 12.9094 14.7891V20.4516H9.35625V8.99531H12.7687V10.5609H12.8156C13.2891 9.66094 14.4516 8.70938 16.1813 8.70938C19.7859 8.70938 20.4516 11.0813 20.4516 14.1656V20.4516Z" fill="#FAFAFA"/>
</g>
<defs>
<clipPath id="clip0_2001_1428">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 989 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.0512 4.07466C15.3113 5.17727 16.301 5.99866 17.4829 5.99866C18.8627 5.99866 19.9813 4.87965 19.9813 3.49933C19.9813 2.11902 18.8627 1 17.4829 1C16.2764 1 15.2703 1.85537 15.036 2.99314C13.0155 3.20991 11.4378 4.92417 11.4378 7.00167C11.4378 7.00636 11.4378 7.00988 11.4378 7.01456C9.24041 7.10713 7.23397 7.73284 5.641 8.72062C5.04949 8.26247 4.30688 7.98945 3.50102 7.98945C1.5672 7.98945 0 9.55725 0 11.4918C0 12.8955 0.824597 14.1048 2.01581 14.6637C2.13177 18.7297 6.56047 22 12.0082 22C17.4559 22 21.8905 18.7261 22.0006 14.6567C23.1824 14.0942 24 12.8885 24 11.493C24 9.55842 22.4328 7.99063 20.499 7.99063C19.6966 7.99063 18.9575 8.2613 18.3672 8.71594C16.7602 7.72113 14.7315 7.09541 12.5119 7.01222C12.5119 7.0087 12.5119 7.00636 12.5119 7.00285C12.5119 5.51473 13.6176 4.27971 15.0512 4.077V4.07466ZM5.50044 13.7146C5.559 12.4444 6.40234 11.4695 7.38272 11.4695C8.3631 11.4695 9.11274 12.4995 9.05417 13.7697C8.99561 15.0398 8.26354 15.5015 7.28199 15.5015C6.30044 15.5015 5.44187 14.9848 5.50044 13.7146ZM16.6348 11.4695C17.6164 11.4695 18.4597 12.4444 18.5171 13.7146C18.5757 14.9848 17.716 15.5015 16.7356 15.5015C15.7552 15.5015 15.022 15.041 14.9634 13.7697C14.9048 12.4995 15.6533 11.4695 16.6348 11.4695ZM15.4682 16.6533C15.6521 16.6721 15.7693 16.8631 15.6978 17.0341C15.0946 18.4766 13.6703 19.4901 12.0082 19.4901C10.3461 19.4901 8.92299 18.4766 8.31859 17.0341C8.24714 16.8631 8.36427 16.6721 8.54817 16.6533C9.62577 16.5444 10.7912 16.4846 12.0082 16.4846C13.2252 16.4846 14.3895 16.5444 15.4682 16.6533Z" fill="#FAFAFA"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.3263 1.90393H21.6998L14.3297 10.3274L23 21.7899H16.2112L10.894 14.838L4.80995 21.7899H1.43443L9.31743 12.78L1 1.90393H7.96111L12.7674 8.25826L18.3263 1.90393ZM17.1423 19.7707H19.0116L6.94539 3.81706H4.93946L17.1423 19.7707Z" fill="#FAFAFA"/>
</svg>

Before

Width:  |  Height:  |  Size: 358 B

+66 -76
View File
@@ -1,5 +1,5 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"$schema": "https://biomejs.dev/schemas/2.1.4/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
@@ -28,19 +28,19 @@
"rules": {
"recommended": true,
"correctness": {
"useExhaustiveDependencies": "info",
"useExhaustiveDependencies": "off",
"noUndeclaredVariables": "off",
"noEmptyPattern": "info",
"noEmptyPattern": "off",
"useJsxKeyInIterable": "off",
"noInnerDeclarations": "off",
"useHookAtTopLevel": "info",
"useYield": "info",
"useHookAtTopLevel": "off",
"useYield": "off",
"noConstructorReturn": "off",
"noInvalidPositionAtImportRule": "off",
"noSwitchDeclarations": "off",
"noUnusedImports": "error"
},
"a11y": "info",
"a11y": "off",
"style": {
"useNodejsImportProtocol": "off",
"useImportType": "off",
@@ -51,36 +51,35 @@
"noParameterAssign": "off",
"useAsConstAssertion": "off",
"useDefaultParameterLast": "off",
"noNonNullAssertion": "info",
"noNonNullAssertion": "off",
"useEnumInitializers": "off",
"useSelfClosingElements": "info",
"useSelfClosingElements": "off",
"useSingleVarDeclarator": "off",
"useNumberNamespace": "info",
"noInferrableTypes": "info",
"useTemplate": "info",
"noUselessElse": "info"
"useNumberNamespace": "off",
"noInferrableTypes": "off",
"useTemplate": "off",
"noUselessElse": "off"
},
"suspicious": {
"noDoubleEquals": "warn",
"noImplicitAnyLet": "info",
"noThenProperty": "off",
"noAsyncPromiseExecutor": "info",
"noAsyncPromiseExecutor": "off",
"noImportAssign": "off",
"noExplicitAny": "info",
"noControlCharactersInRegex": "warn",
"noExplicitAny": "off",
"noControlCharactersInRegex": "off",
"noShadowRestrictedNames": "off",
"noArrayIndexKey": "info",
"noAssignInExpressions": "info",
"useIterableCallbackReturn": "info"
"noAssignInExpressions": "info"
},
"complexity": {
"noUselessConstructor": "info",
"useOptionalChain": "info",
"noBannedTypes": "warn",
"useLiteralKeys": "info",
"noUselessCatch": "info",
"noUselessSwitchCase": "info",
"noStaticOnlyClass": "info"
"noUselessConstructor": "off",
"useOptionalChain": "off",
"noBannedTypes": "off",
"useLiteralKeys": "off",
"noUselessCatch": "off",
"noUselessSwitchCase": "off",
"noStaticOnlyClass": "off"
},
"security": {
"noDangerouslySetInnerHtml": "info"
@@ -95,11 +94,6 @@
"lineEnding": "lf",
"formatWithErrors": true
},
"css": {
"parser": {
"tailwindDirectives": true
}
},
"javascript": {
"formatter": {
"semicolons": "asNeeded",
@@ -118,21 +112,19 @@
}
},
"files": {
"ignoreUnknown": true,
"includes": [
"**",
// explicitly force files to be ignored by the scanner with !!
"!!**/dist",
"!!**/dist-*",
"!!**/out",
"!!**/evals",
"!!**/playwright",
"!!**/test-results",
"!!**/node_modules",
"!!**/webview-ui/build",
"!!**/generated",
"!!**/proto",
"!!**/tests/specs"
"!**/dist",
"!**/dist-*",
"!**/out",
"!**/evals",
"!**/playwright",
"!**/test-results",
"!**/node_modules",
"!**/webview-ui/build",
"!**/generated",
"!**/proto",
"!**/tests/specs"
]
},
"plugins": [
@@ -142,15 +134,14 @@
{
"includes": [
"**",
"!!**/dist",
"!!**/hosts/vscode/**",
"!!**/test/**",
"!!**/*.test.ts",
"!!src/dev/**",
"!!src/extension.ts",
"!!src/integrations/git/commit-message-generator.ts",
"!!src/integrations/terminal/**",
"!!src/core/controller/ui/openWalkthrough.ts"
"!**/hosts/vscode/**",
"!**/test/**",
"!**/*.test.ts",
"!src/dev/**",
"!src/extension.ts",
"!src/integrations/git/commit-message-generator.ts",
"!src/integrations/terminal/**",
"!src/core/controller/ui/openWalkthrough.ts"
],
"plugins": [
"src/dev/grit/vscode-api.grit"
@@ -163,37 +154,36 @@
],
"includes": [
"**",
"!!**/esbuild.*",
"!!**/*.mts",
"!!**/webview-ui/**",
"!!**/evals/**",
"!!**/standalone/**",
"!!**/cli/**",
"!!**/e2e/**",
"!!**/test/**",
"!!**/__tests__/**",
"!!**/*.test.ts",
"!!**/*.stories.ts",
"!!src/dev/**",
"!!**/*.mjs",
"!!**/*.js",
"!!**/scripts/**",
"!!**/*.tsx",
"!!**/testing-platform/**",
"!**/esbuild.*",
"!**/*.mts",
"!**/webview-ui/**",
"!**/evals/**",
"!**/standalone/**",
"!**/e2e/**",
"!**/test/**",
"!**/__tests__/**",
"!**/*.test.ts",
"!**/*.stories.ts",
"!src/dev/**",
"!**/*.mjs",
"!**/*.js",
"!**/scripts/**",
"!**/*.tsx",
"!**/testing-platform/**",
// ACP mode must redirect console to stderr - this is intentional
"!!cli/src/acp/index.ts"
"!cli-ts/src/acp/index.ts"
]
},
{
"includes": [
"**",
"!!src/core/storage/state-migrations.ts",
"!!src/core/storage/FileContextTracker.ts",
"!!src/core/context/context-tracking/FileContextTracker.ts",
"!!src/common.ts",
"!!src/services/logging/distinctId.ts",
"!!src/core/storage/utils/state-helpers.ts",
"!!src/extension.ts"
"!src/core/storage/state-migrations.ts",
"!src/core/storage/FileContextTracker.ts",
"!src/core/context/context-tracking/FileContextTracker.ts",
"!src/common.ts",
"!src/services/logging/distinctId.ts",
"!src/core/storage/utils/state-helpers.ts",
"!src/extension.ts"
],
"plugins": [
"src/dev/grit/use-cache-service.grit"
+94 -105
View File
@@ -1,6 +1,6 @@
# Cline CLI
# Cline CLI (TypeScript)
The official CLI for Cline. Run Cline tasks directly from the terminal with the same underlying functionality as the VS Code extension.
A TypeScript CLI implementation of Cline that reuses the core TypeScript codebase. This allows you to run Cline tasks directly from the terminal while sharing the same underlying functionality as the VS Code extension.
## Features
@@ -27,8 +27,23 @@ npm run install:all
# Ensure protos are generated
npm run protos
# Build the CLI
npm run build:cli
```
Or install the CLI globally:
```bash
# Install all dependencies first
npm run install:all
# Ensure protos are generated
npm run protos
# Build and link the CLI globally
npm run cli:link
cd cli-ts
npm install
npm run link
```
## Usage
@@ -188,51 +203,54 @@ These options are available for the default command (running a task directly):
## Development
### Quick Start
For active development, at the root of this repo:
1. **Initial setup:**
```bash
npm run install:all
npm run protos
```
2. **Make changes to cli-ts:**
```bash
npm run cli:dev
# or
cd cli-ts && npm run dev
```
3. **Test your changes:**
```bash
cline [your-command] # In a new Terminal
```
4. **When done:**
```bash
npm run cli:unlink
```
### Build
```bash
# 1. Install all dependencies (root, webview-ui, cli)
npm run install:all
cd cli-ts
# 2. Build and link globally so you can run `cline` from anywhere
npm run cli:link
# Development build with source maps
npm run build
# 3. Test it
cline --help
# Production build (minified)
npm run build:production
```
### Scripts
Run these from the repository root:
| Script | Description |
|--------|-------------|
| `npm run install:all` | Install deps for root, webview-ui, and cli |
| `npm run cli:build` | Generate protos and build CLI |
| `npm run cli:build:production` | Production build (minified) |
| `npm run cli:link` | Build and `npm link` so you can run `cline` from anywhere |
| `npm run cli:unlink` | Remove the global `cline` symlink |
| `npm run cli:dev` | Link + watch mode for development |
| `npm run cli:watch` | Watch mode only (no initial build) |
| `npm run cli:test` | Run CLI tests |
### Development Workflow
1. Run `npm run cli:dev` - this links the CLI globally and starts watch mode
2. Make changes to files in `cli/src/`
3. The build automatically rebuilds on save
4. Test your changes by running `cline` in another terminal
5. When done, run `npm run cli:unlink` to clean up
### Proto Generation
The CLI uses proto-generated types for message passing (same as the VS Code extension). If you modify any `.proto` files, run:
### Watch Mode
```bash
npm run protos
npm run watch
```
This generates TypeScript types in `src/generated/` that both the CLI and extension use.
### Type Checking
```bash
npm run typecheck
```
## Publish
@@ -250,7 +268,7 @@ npm run update-brew-formula
```bash
# Create a local tap
brew tap-new cline/local
cp ./cli/cline.rb "$(brew --repository)/Library/Taps/cline/homebrew-local/Formula/cline.rb"
cp ./cli-ts/cline.rb "$(brew --repository)/Library/Taps/cline/homebrew-local/Formula/cline.rb"
# Build from Source
brew install --build-from-source cline/local/cline
@@ -271,50 +289,19 @@ git push
## Architecture
### How It Works
The CLI reuses the core Cline TypeScript codebase:
The CLI directly imports and reuses the core Cline TypeScript codebase (the same code that powers the VS Code extension). This means feature parity is easy to maintain - when core gets updated, the CLI automatically benefits.
- **Controller** (`@core/controller`): Manages task lifecycle and state
- **Task** (`@core/task`): Executes Cline tasks using the AI API
- **StateManager** (`@core/storage`): Handles persistent state storage
```
┌─────────────────────────────────────────────────────────┐
│ CLI (cli/) │
│ - React Ink terminal UI │
│ - Command parsing (commander) │
│ - Terminal-specific adapters │
└─────────────────────────────────────────────────────────┘
│ direct imports
┌─────────────────────────────────────────────────────────┐
│ Core (src/core/) │
│ - Controller: task lifecycle, state management │
│ - Task: AI API calls, tool execution │
│ - StateManager: persistent storage │
│ - Proto types: message definitions │
└─────────────────────────────────────────────────────────┘
```
CLI-specific implementations:
Unlike a client-server architecture, the CLI runs everything in a single Node.js process. The "host bridge" pattern provides terminal-appropriate implementations for things the VS Code extension would handle differently (clipboard, file dialogs, etc.).
### Key Files
| File | Purpose |
|------|---------|
| `src/index.ts` | Entry point, command definitions |
| `src/components/App.tsx` | Main React Ink app |
| `src/components/ChatView.tsx` | Task conversation UI |
| `src/controllers/CliWebviewProvider.ts` | Bridges core messages to terminal output |
| `src/vscode-context.ts` | Mock VS Code extension context for core compatibility |
| `src/vscode-shim.ts` | Shims for VS Code APIs that core depends on |
| `src/constants/colors.ts` | Terminal color definitions |
### React Ink
The CLI uses [React Ink](https://github.com/vadimdemedes/ink) for its terminal UI. This lets us build the interface with React components that render to the terminal. Key patterns:
- Components in `src/components/` render terminal UI
- Hooks in `src/hooks/` manage terminal-specific state (size, scrolling)
- The `useStateSubscriber` hook subscribes to core state changes
- `cli-host-bridge.ts`: CLI implementations of host bridge services
- `cli-webview-provider.ts`: WebviewProvider that outputs to terminal
- `cli-comment-review.ts`: Comment review controller for terminal
- `vscode-context.ts`: Mock VSCode extension context
- `display.ts`: Terminal output formatting utilities
## Configuration
@@ -327,39 +314,41 @@ The CLI stores its data in `~/.cline/data/` by default:
Override with the `--config` option or `CLINE_DIR` environment variable.
## Comparison with Go CLI
This TypeScript CLI differs from the Go CLI (`cli/` directory):
| Feature | Go CLI | TypeScript CLI |
|---------|--------|----------------|
| Language | Go | TypeScript |
| Core sharing | Uses gRPC to communicate | Direct imports |
| Startup time | Fast | Moderate |
| Dependencies | Standalone binary | Requires Node.js |
| Best for | Production deployment | Development, debugging |
Choose the TypeScript CLI when you need to debug or modify the core Cline logic. Choose the Go CLI for production deployment with faster startup.
## Troubleshooting
### Build Errors
If you encounter build errors:
If you encounter build errors, ensure you've:
1. Run `npm install` in the repository root
2. Run `npm run protos` to generate proto files
3. Have all peer dependencies installed
### Missing Dependencies
The CLI imports from the parent project. If you see import errors:
```bash
# Make sure all deps are installed
npm run install:all
# Regenerate proto types
cd .. # Go to repository root
npm install
npm run protos
# Then rebuild
npm run cli:build
```
### "command not found: cline"
The CLI isn't linked globally. Run:
### Permission Denied
Make the CLI executable:
```bash
npm run cli:link
```
### Changes Not Reflected
If your code changes aren't showing up:
1. Make sure watch mode is running (`npm run cli:dev`)
2. Check for TypeScript errors in the watch output
3. Try unlinking and relinking: `npm run cli:unlink && npm run cli:link`
### Import Errors from Core
The CLI imports from `@core/`, `@shared/`, etc. These paths are defined in the root `tsconfig.json`. If you see import errors, make sure you're building from the repo root, not from inside `cli/`.
chmod +x dist/cli.js
```
-1
View File
@@ -7,7 +7,6 @@ class Cline < Formula
license "Apache-2.0"
depends_on "node@20"
depends_on "ripgrep"
def install
system "npm", "install", *std_npm_args(prefix: false)
+4 -5
View File
@@ -102,7 +102,7 @@ const esbuildProblemMatcherPlugin: esbuild.Plugin = {
name: "esbuild-problem-matcher",
setup(build) {
build.onStart(() => {
console.log("[cli esbuild] Build started...")
console.log("[cli-ts esbuild] Build started...")
})
build.onEnd((result) => {
result.errors.forEach(({ text, location }) => {
@@ -111,7 +111,7 @@ const esbuildProblemMatcherPlugin: esbuild.Plugin = {
console.error(` ${location.file}:${location.line}:${location.column}:`)
}
})
console.log("[cli esbuild] Build finished")
console.log("[cli-ts esbuild] Build finished")
})
},
}
@@ -199,7 +199,7 @@ const buildTimeEnvs = [
buildTimeEnvs.forEach((envVar) => {
if (process.env[envVar]) {
console.log(`[cli esbuild] ${envVar} env var is set`)
console.log(`[cli-ts esbuild] ${envVar} env var is set`)
buildEnvVars[`process.env.${envVar}`] = JSON.stringify(process.env[envVar])
}
})
@@ -234,7 +234,6 @@ const config: esbuild.BuildOptions = {
"aws4fetch",
"pino",
"pino-roll",
"@vscode/ripgrep", // Uses __dirname to locate the binary
],
supported: { "top-level-await": true },
banner: {
@@ -254,7 +253,7 @@ async function main() {
const ctx = await esbuild.context(config)
if (watch) {
await ctx.watch()
console.log("[cli] Watching for changes...")
console.log("[cli-ts] Watching for changes...")
} else {
await ctx.rebuild()
await ctx.dispose()
+384
View File
@@ -0,0 +1,384 @@
.\" Automatically generated by Pandoc 3.8.3
.\"
.TH "CLINE" "1" "January 2026" "Cline CLI 2.0" "User Commands"
.SH NAME
cline \- AI coding assistant in your terminal
.SH SYNOPSIS
\f[B]cline\f[R] [\f[I]prompt\f[R]] [\f[I]options\f[R]]
.PP
\f[B]cline\f[R] \f[I]command\f[R] [\f[I]options\f[R]]
[\f[I]arguments\f[R]]
.SH DESCRIPTION
\f[B]cline\f[R] is a command\-line interface for the Cline AI coding
assistant.
It provides the same powerful AI capabilities as the VS Code extension,
directly in your terminal.
.PP
Cline is an autonomous AI agent that can read, write, and execute code
across your projects.
He can create and edit files, run terminal commands, use a headless
browser, and more\(emall while asking for your approval before taking
actions.
.PP
The CLI supports both interactive mode (with a rich terminal UI) and
plain text mode (for piped input and scripted workflows).
.SH MODES OF OPERATION
\f[B]Interactive Mode\f[R] : When you run \f[B]cline\f[R] without
arguments, it launches an interactive welcome prompt with a rich
terminal UI.
You can type your task, view conversation history, and interact with
Cline in real\-time.
.PP
\f[B]Task Mode\f[R] : Run \f[B]cline \(lqprompt\(rq\f[R] or \f[B]cline
task \(lqprompt\(rq\f[R] to immediately start a task.
If stdin is a TTY, you\(cqll see the interactive UI.
If stdin is piped or output is redirected, the CLI automatically
switches to plain text mode.
.PP
\f[B]Plain Text Mode\f[R] : Activated automatically when stdin is piped,
output is redirected, or \f[B]\-\-json\f[R]/\f[B]\-\-yolo\f[R] flags are
used.
Outputs clean text without the Ink UI, suitable for scripting and CI/CD
pipelines.
.SH AGENT BEHAVIOR
Cline operates in two primary modes:
.PP
\f[B]ACT MODE\f[R] : Cline actively uses tools to accomplish tasks.
He can read files, write code, execute commands, use a headless browser,
and more.
This is the default mode for task execution.
.PP
\f[B]PLAN MODE\f[R] : Cline gathers information and creates a detailed
plan before implementation.
He explores the codebase, asks clarifying questions, and presents a
strategy for user approval before switching to ACT MODE.
.SH COMMANDS
.SS task (alias: t)
Run a new task with a prompt.
.PP
\f[B]cline task\f[R] \f[I]prompt\f[R] [\f[I]options\f[R]]
.PP
\f[B]cline t\f[R] \f[I]prompt\f[R] [\f[I]options\f[R]] : Create and run
a new task.
Options:
.PP
\f[B]\-a\f[R], \f[B]\-\-act\f[R] : Run in act mode (default)
.PP
\f[B]\-p\f[R], \f[B]\-\-plan\f[R] : Run in plan mode
.PP
\f[B]\-y\f[R], \f[B]\-\-yolo\f[R] : Enable yolo/yes mode (auto\-approve
all actions, output in plain mode, exit process automatically when task
complete)
.PP
\f[B]\-m\f[R], \f[B]\-\-model\f[R] \f[I]model\f[R] : Model to use for
the task
.PP
\f[B]\-i\f[R], \f[B]\-\-images\f[R] \f[I]paths\&...\f[R] : Image file
paths to include with the task
.PP
\f[B]\-v\f[R], \f[B]\-\-verbose\f[R] : Show verbose output including
reasoning
.PP
\f[B]\-c\f[R], \f[B]\-\-cwd\f[R] \f[I]path\f[R] : Working directory for
the task
.PP
\f[B]\-\-config\f[R] \f[I]path\f[R] : Path to Cline configuration
directory
.PP
\f[B]\-\-thinking\f[R] : Enable extended thinking (1024 token budget)
.PP
\f[B]\-\-json\f[R] : Output messages as JSON instead of styled text
.SS history (alias: h)
List task history with pagination.
.PP
\f[B]cline history\f[R] [\f[I]options\f[R]]
.PP
\f[B]cline h\f[R] [\f[I]options\f[R]] : Display previous tasks.
Options:
.PP
\f[B]\-n\f[R], \f[B]\-\-limit\f[R] \f[I]number\f[R] : Number of tasks to
show (default: 10)
.PP
\f[B]\-p\f[R], \f[B]\-\-page\f[R] \f[I]number\f[R] : Page number,
1\-based (default: 1)
.PP
\f[B]\-\-config\f[R] \f[I]path\f[R] : Path to Cline configuration
directory
.SS config
Show current configuration.
.PP
\f[B]cline config\f[R] [\f[I]options\f[R]] : Display global and
workspace state.
Options:
.PP
\f[B]\-\-config\f[R] \f[I]path\f[R] : Path to Cline configuration
directory
.SS auth
Authenticate a provider and configure the model.
.PP
\f[B]cline auth\f[R] [\f[I]options\f[R]] : Launch interactive
authentication wizard, or use quick setup flags.
Options:
.PP
\f[B]\-p\f[R], \f[B]\-\-provider\f[R] \f[I]id\f[R] : Provider ID for
quick setup (e.g., openai\-native, anthropic, openrouter)
.PP
\f[B]\-k\f[R], \f[B]\-\-apikey\f[R] \f[I]key\f[R] : API key for the
provider
.PP
\f[B]\-m\f[R], \f[B]\-\-modelid\f[R] \f[I]id\f[R] : Model ID to
configure (e.g., gpt\-4o, claude\-sonnet\-4\-5\-20250929)
.PP
\f[B]\-b\f[R], \f[B]\-\-baseurl\f[R] \f[I]url\f[R] : Base URL (optional,
for OpenAI\-compatible providers)
.PP
\f[B]\-v\f[R], \f[B]\-\-verbose\f[R] : Show verbose output
.PP
\f[B]\-c\f[R], \f[B]\-\-cwd\f[R] \f[I]path\f[R] : Working directory
.PP
\f[B]\-\-config\f[R] \f[I]path\f[R] : Path to Cline configuration
directory
.SS update
Check for updates and install if available.
.PP
\f[B]cline update\f[R] [\f[I]options\f[R]] : Check npm for newer
versions.
Options:
.PP
\f[B]\-v\f[R], \f[B]\-\-verbose\f[R] : Show verbose output
.SS version
Show the CLI version number.
.PP
\f[B]cline version\f[R]
.SS dev
Developer tools and utilities.
.PP
\f[B]cline dev log\f[R] : Open the log file for debugging.
.SH DEFAULT COMMAND OPTIONS
When running \f[B]cline\f[R] with just a prompt (no subcommand), these
options are available:
.PP
\f[B]\-a\f[R], \f[B]\-\-act\f[R] : Run in act mode (default)
.PP
\f[B]\-p\f[R], \f[B]\-\-plan\f[R] : Run in plan mode
.PP
\f[B]\-y\f[R], \f[B]\-\-yolo\f[R] : Enable yolo mode (auto\-approve all
actions).
Also forces plain text output mode.
.PP
\f[B]\-m\f[R], \f[B]\-\-model\f[R] \f[I]model\f[R] : Model to use for
the task
.PP
\f[B]\-v\f[R], \f[B]\-\-verbose\f[R] : Show verbose output
.PP
\f[B]\-c\f[R], \f[B]\-\-cwd\f[R] \f[I]path\f[R] : Working directory
.PP
\f[B]\-\-config\f[R] \f[I]path\f[R] : Configuration directory
.PP
\f[B]\-\-thinking\f[R] : Enable extended thinking (1024 token budget)
.PP
\f[B]\-\-json\f[R] : Output messages as JSON instead of styled text.
Forces plain text mode.
.SH JSON OUTPUT FORMAT
When using \f[B]\-\-json\f[R], each message is output as a JSON object
with these fields:
.PP
\f[B]Required fields:\f[R]
.IP \(bu 2
\f[B]type\f[R]: \(lqask\(rq or \(lqsay\(rq
.IP \(bu 2
\f[B]text\f[R]: message text
.IP \(bu 2
\f[B]ts\f[R]: Unix epoch timestamp in milliseconds
.PP
\f[B]Optional fields:\f[R]
.IP \(bu 2
\f[B]reasoning\f[R]: reasoning text
.IP \(bu 2
\f[B]say\f[R]: say subtype (when type is \(lqsay\(rq)
.IP \(bu 2
\f[B]ask\f[R]: ask subtype (when type is \(lqask\(rq)
.IP \(bu 2
\f[B]partial\f[R]: streaming flag
.IP \(bu 2
\f[B]images\f[R]: list of image URIs
.IP \(bu 2
\f[B]files\f[R]: list of file paths
.SH EXAMPLES
.SS Basic Usage
.IP
.EX
\f[I]# Launch interactive mode\f[R]
cline
\f[I]# Run a task directly\f[R]
cline \(dqCreate a hello world function in Python\(dq
\f[I]# Run with verbose output and extended thinking\f[R]
cline \-v \-\-thinking \(dqAnalyze this codebase architecture\(dq
.EE
.SS Mode Selection
.IP
.EX
\f[I]# Run in plan mode (gather info before acting)\f[R]
cline \-p \(dqDesign a REST API for user management\(dq
\f[I]# Run in act mode with auto\-approval (yolo)\f[R]
cline \-y \(dqFix the typo in README.md\(dq
.EE
.SS Using Specific Models
.IP
.EX
\f[I]# Use a specific model\f[R]
cline \-m claude\-sonnet\-4\-5\-20250929 \(dqRefactor this function\(dq
\f[I]# Quick auth setup with model\f[R]
cline auth \-p anthropic \-k sk\-ant\-xxxxx \-m claude\-sonnet\-4\-5\-20250929
.EE
.SS Including Images
.IP
.EX
\f[I]# Include images with explicit flag\f[R]
cline task \-i screenshot.png diagram.jpg \(dqFix the UI based on these images\(dq
\f[I]# Or use inline image references in the prompt\f[R]
cline \(dqFix the layout shown in \(at./screenshot.png\(dq
.EE
.SS Piped Input
.IP
.EX
\f[I]# Pipe file contents to Cline\f[R]
cat README.md \f[B]|\f[R] cline \(dqSummarize this document\(dq
\f[I]# Pipe with additional prompt\f[R]
echo \(dqfunction add(a, b) { return a + b }\(dq \f[B]|\f[R] cline \(dqAdd TypeScript types to this\(dq
\f[I]# Combine piped input with a prompt\f[R]
git diff \f[B]|\f[R] cline \(dqReview these changes and suggest improvements\(dq
.EE
.SS Scripting and Automation
.IP
.EX
\f[I]# JSON output for parsing\f[R]
cline \-\-json \(dqWhat files are in this directory?\(dq \f[B]|\f[R] jq \(aq.text\(aq
\f[I]# Yolo mode for automated workflows (auto\-approves all actions), forces plain text output\f[R]
cline \-y \(dqRun the test suite and fix any failures\(dq
.EE
.SS Task History
.IP
.EX
\f[I]# List recent tasks\f[R]
cline history
\f[I]# Show more tasks with pagination\f[R]
cline history \-n 20 \-p 2
.EE
.SS Authentication
.IP
.EX
\f[I]# Interactive authentication wizard\f[R]
cline auth
\f[I]# Quick setup for Anthropic\f[R]
cline auth \-p anthropic \-k sk\-ant\-api\-xxxxx
\f[I]# Quick setup for OpenAI\f[R]
cline auth \-p openai\-native \-k sk\-xxxxx \-m gpt\-4o
\f[I]# OpenAI\-compatible provider with custom base URL\f[R]
cline auth \-p openai \-k your\-api\-key \-b https://api.example.com/v1
.EE
.SH ENVIRONMENT
\f[B]CLINE_DIR\f[R] : Override the default configuration directory.
When set, Cline stores all data in this directory instead of
\f[CR]\(ti/.cline/data/\f[R].
.PP
\f[B]CLINE_COMMAND_PERMISSIONS\f[R] : JSON configuration for restricting
which shell commands Cline can execute.
When set, commands are validated against allow/deny patternks before
execution.
When not set, all commands are allowed.
.PP
Format:
\f[CR]{\(dqallow\(dq: [\(dqpattern1\(dq, \(dqpattern2\(dq], \(dqdeny\(dq: [\(dqpattern3\(dq], \(dqallowRedirects\(dq: true}\f[R]
.PP
\f[B]Fields:\f[R]
.IP \(bu 2
\f[B]allow\f[R] (array of strings): Glob patterns for allowed commands.
If specified, only matching commands are permitted.
Uses \f[CR]*\f[R] to match any characters and \f[CR]?\f[R] to match a
single character.
Setting allow on anything will deny all others.
.IP \(bu 2
\f[B]deny\f[R] (array of strings): Glob patterns for denied commands.
Deny rules take precedence over allow rules.
.IP \(bu 2
\f[B]allowRedirects\f[R] (boolean): Whether to allow shell redirects
(\f[CR]>\f[R], \f[CR]>>\f[R], \f[CR]<\f[R], etc.).
Defaults to false.
.PP
\f[B]Rule evaluation:\f[R]
.IP "1." 3
Check for dangerous characters (backticks outside single quotes,
unquoted newlines)
.IP "2." 3
Parse command into segments split by operators (\f[CR]&&\f[R],
\f[CR]||\f[R], \f[CR]|\f[R], \f[CR];\f[R])
.IP "3." 3
If redirects detected and \f[CR]allowRedirects\f[R] is not true, command
is denied
.IP "4." 3
Each segment is validated against deny rules first, then allow rules
.IP "5." 3
Subshell contents (\f[CR]$(...)\f[R] and \f[CR](...)\f[R]) are
recursively validated
.IP "6." 3
All segments must pass for the command to be allowed
.PP
\f[B]Examples:\f[R]
.IP
.EX
\f[I]# Allow only npm and git commands.\f[R]
export CLINE_COMMAND_PERMISSIONS=\(aq{\(dqallow\(dq: [\(dqnpm *\(dq, \(dqgit *\(dq]}\(aq
\f[I]# Allow development commands but deny dangerous ones. Deny not strictly required here since allow is set.\f[R]
export CLINE_COMMAND_PERMISSIONS=\(aq{\(dqallow\(dq: [\(dqnpm *\(dq, \(dqgit *\(dq, \(dqnode *\(dq], \(dqdeny\(dq: [\(dqrm \-rf *\(dq, \(dqsudo *\(dq]}\(aq
\f[I]# Allow file operations with redirects\f[R]
export CLINE_COMMAND_PERMISSIONS=\(aq{\(dqallow\(dq: [\(dqcat *\(dq, \(dqecho *\(dq], \(dqallowRedirects\(dq: true}\(aq
.EE
.SH FILES
\f[B]\(ti/.cline/data/\f[R] : Default configuration directory
containing:
.PP
\f[B]globalState.json\f[R] : Global settings and state
.PP
\f[B]secrets.json\f[R] : API keys and secrets (stored securely)
.PP
\f[B]workspace/\f[R] : Workspace\-specific state
.PP
\f[B]tasks/\f[R] : Task history and conversation data
.PP
\f[B]\(ti/.cline/log/\f[R] : Log files for debugging.
View with \f[CR]cline dev log\f[R].
.SH BUGS
Report bugs at: \c
.UR https://github.com/cline/cline/issues
.UE \c
.PP
For real\-time help, join the Discord community at: \c
.UR https://discord.gg/cline
.UE \c
.SH SEE ALSO
Full documentation: \c
.UR https://docs.cline.bot
.UE \c
.PP
VS Code extension: \c
.UR https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev
.UE \c
.SH AUTHORS
Cline is developed by Cline Bot Inc.\ and the open source community.
.SH COPYRIGHT
Copyright © 2025 Cline Bot Inc.\ Licensed under the Apache License 2.0.
+340
View File
@@ -0,0 +1,340 @@
---
title: CLINE
section: 1
header: User Commands
footer: Cline CLI 2.0
date: January 2026
---
# NAME
cline - AI coding assistant in your terminal
# SYNOPSIS
**cline** [*prompt*] [*options*]
**cline** *command* [*options*] [*arguments*]
# DESCRIPTION
**cline** is a command-line interface for the Cline AI coding assistant. It provides the same powerful AI capabilities as the VS Code extension, directly in your terminal.
Cline is an autonomous AI agent that can read, write, and execute code across your projects. He can create and edit files, run terminal commands, use a headless browser, and more—all while asking for your approval before taking actions.
The CLI supports both interactive mode (with a rich terminal UI) and plain text mode (for piped input and scripted workflows).
# MODES OF OPERATION
**Interactive Mode** : When you run **cline** without arguments, it launches an interactive welcome prompt with a rich terminal UI. You can type your task, view conversation history, and interact with Cline in real-time.
**Task Mode** : Run **cline "prompt"** or **cline task "prompt"** to immediately start a task. If stdin is a TTY, you'll see the interactive UI. If stdin is piped or output is redirected, the CLI automatically switches to plain text mode.
**Plain Text Mode** : Activated automatically when stdin is piped, output is redirected, or **\--json**/**\--yolo** flags are used. Outputs clean text without the Ink UI, suitable for scripting and CI/CD pipelines.
# AGENT BEHAVIOR
Cline operates in two primary modes:
**ACT MODE** : Cline actively uses tools to accomplish tasks. He can read files, write code, execute commands, use a headless browser, and more. This is the default mode for task execution.
**PLAN MODE** : Cline gathers information and creates a detailed plan before implementation. He explores the codebase, asks clarifying questions, and presents a strategy for user approval before switching to ACT MODE.
# COMMANDS
## task (alias: t)
Run a new task with a prompt.
**cline task** *prompt* [*options*]
**cline t** *prompt* [*options*] : Create and run a new task. Options:
**-a**, **\--act** : Run in act mode (default)
**-p**, **\--plan** : Run in plan mode
**-y**, **\--yolo** : Enable yolo/yes mode (auto-approve all actions, output in plain mode, exit process automatically when task complete)
**-m**, **\--model** *model* : Model to use for the task
**-i**, **\--images** *paths...* : Image file paths to include with the task
**-v**, **\--verbose** : Show verbose output including reasoning
**-c**, **\--cwd** *path* : Working directory for the task
**\--config** *path* : Path to Cline configuration directory
**\--thinking** : Enable extended thinking (1024 token budget)
**\--json** : Output messages as JSON instead of styled text
## history (alias: h)
List task history with pagination.
**cline history** [*options*]
**cline h** [*options*] : Display previous tasks. Options:
**-n**, **\--limit** *number* : Number of tasks to show (default: 10)
**-p**, **\--page** *number* : Page number, 1-based (default: 1)
**\--config** *path* : Path to Cline configuration directory
## config
Show current configuration.
**cline config** [*options*] : Display global and workspace state. Options:
**\--config** *path* : Path to Cline configuration directory
## auth
Authenticate a provider and configure the model.
**cline auth** [*options*] : Launch interactive authentication wizard, or use quick setup flags. Options:
**-p**, **\--provider** *id* : Provider ID for quick setup (e.g., openai-native, anthropic, openrouter)
**-k**, **\--apikey** *key* : API key for the provider
**-m**, **\--modelid** *id* : Model ID to configure (e.g., gpt-4o, claude-sonnet-4-5-20250929)
**-b**, **\--baseurl** *url* : Base URL (optional, for OpenAI-compatible providers)
**-v**, **\--verbose** : Show verbose output
**-c**, **\--cwd** *path* : Working directory
**\--config** *path* : Path to Cline configuration directory
## update
Check for updates and install if available.
**cline update** [*options*] : Check npm for newer versions. Options:
**-v**, **\--verbose** : Show verbose output
## version
Show the CLI version number.
**cline version**
## dev
Developer tools and utilities.
**cline dev log** : Open the log file for debugging.
# DEFAULT COMMAND OPTIONS
When running **cline** with just a prompt (no subcommand), these options are available:
**-a**, **\--act** : Run in act mode (default)
**-p**, **\--plan** : Run in plan mode
**-y**, **\--yolo** : Enable yolo mode (auto-approve all actions). Also forces plain text output mode.
**-m**, **\--model** *model* : Model to use for the task
**-v**, **\--verbose** : Show verbose output
**-c**, **\--cwd** *path* : Working directory
**\--config** *path* : Configuration directory
**\--thinking** : Enable extended thinking (1024 token budget)
**\--json** : Output messages as JSON instead of styled text. Forces plain text mode.
# JSON OUTPUT FORMAT
When using **\--json**, each message is output as a JSON object with these fields:
**Required fields:**
- **type**: "ask" or "say"
- **text**: message text
- **ts**: Unix epoch timestamp in milliseconds
**Optional fields:**
- **reasoning**: reasoning text
- **say**: say subtype (when type is "say")
- **ask**: ask subtype (when type is "ask")
- **partial**: streaming flag
- **images**: list of image URIs
- **files**: list of file paths
# EXAMPLES
## Basic Usage
```bash
# Launch interactive mode
cline
# Run a task directly
cline "Create a hello world function in Python"
# Run with verbose output and extended thinking
cline -v --thinking "Analyze this codebase architecture"
```
## Mode Selection
```bash
# Run in plan mode (gather info before acting)
cline -p "Design a REST API for user management"
# Run in act mode with auto-approval (yolo)
cline -y "Fix the typo in README.md"
```
## Using Specific Models
```bash
# Use a specific model
cline -m claude-sonnet-4-5-20250929 "Refactor this function"
# Quick auth setup with model
cline auth -p anthropic -k sk-ant-xxxxx -m claude-sonnet-4-5-20250929
```
## Including Images
```bash
# Include images with explicit flag
cline task -i screenshot.png diagram.jpg "Fix the UI based on these images"
# Or use inline image references in the prompt
cline "Fix the layout shown in @./screenshot.png"
```
## Piped Input
```bash
# Pipe file contents to Cline
cat README.md | cline "Summarize this document"
# Pipe with additional prompt
echo "function add(a, b) { return a + b }" | cline "Add TypeScript types to this"
# Combine piped input with a prompt
git diff | cline "Review these changes and suggest improvements"
```
## Scripting and Automation
```bash
# JSON output for parsing
cline --json "What files are in this directory?" | jq '.text'
# Yolo mode for automated workflows (auto-approves all actions), forces plain text output
cline -y "Run the test suite and fix any failures"
```
## Task History
```bash
# List recent tasks
cline history
# Show more tasks with pagination
cline history -n 20 -p 2
```
## Authentication
```bash
# Interactive authentication wizard
cline auth
# Quick setup for Anthropic
cline auth -p anthropic -k sk-ant-api-xxxxx
# Quick setup for OpenAI
cline auth -p openai-native -k sk-xxxxx -m gpt-4o
# OpenAI-compatible provider with custom base URL
cline auth -p openai -k your-api-key -b https://api.example.com/v1
```
# ENVIRONMENT
**CLINE_DIR** : Override the default configuration directory. When set, Cline stores all data in this directory instead of `~/.cline/data/`.
**CLINE_COMMAND_PERMISSIONS** : JSON configuration for restricting which shell commands Cline can execute. When set, commands are validated against allow/deny patternks before execution. When not set, all commands are allowed.
Format: `{"allow": ["pattern1", "pattern2"], "deny": ["pattern3"], "allowRedirects": true}`
**Fields:**
- **allow** (array of strings): Glob patterns for allowed commands. If specified, only matching commands are permitted. Uses `*` to match any characters and `?` to match a single character. Setting allow on anything will deny all others.
- **deny** (array of strings): Glob patterns for denied commands. Deny rules take precedence over allow rules.
- **allowRedirects** (boolean): Whether to allow shell redirects (`>`, `>>`, `<`, etc.). Defaults to false.
**Rule evaluation:**
1. Check for dangerous characters (backticks outside single quotes, unquoted newlines)
2. Parse command into segments split by operators (`&&`, `||`, `|`, `;`)
3. If redirects detected and `allowRedirects` is not true, command is denied
4. Each segment is validated against deny rules first, then allow rules
5. Subshell contents (`$(...)` and `(...)`) are recursively validated
6. All segments must pass for the command to be allowed
**Examples:**
```bash
# Allow only npm and git commands.
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *"]}'
# Allow development commands but deny dangerous ones. Deny not strictly required here since allow is set.
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *", "node *"], "deny": ["rm -rf *", "sudo *"]}'
# Allow file operations with redirects
export CLINE_COMMAND_PERMISSIONS='{"allow": ["cat *", "echo *"], "allowRedirects": true}'
```
# CONFIGURATION FILES
```
~/.cline/
├── data/ # Default configuration directory
│ ├── globalState.json # Global settings and state
│ ├── secrets.json # API keys and secrets (stored securely)
│ ├── workspace/ # Workspace-specific state
│ └── tasks/ # Task history and conversation data
└── log/ # Log files for debugging
```
View logs with `cline dev log`.
# BUGS
Report bugs at: <https://github.com/cline/cline/issues>
For real-time help, join the Discord community at: <https://discord.gg/cline>
# SEE ALSO
Full documentation: <https://docs.cline.bot>
VS Code extension: <https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev>
# AUTHORS
Cline is developed by Cline Bot Inc. and the open source community.
# COPYRIGHT
Copyright © 2025 Cline Bot Inc. Licensed under the Apache License 2.0.
+2950
View File
File diff suppressed because it is too large Load Diff
+81
View File
@@ -0,0 +1,81 @@
{
"name": "@cline/cli",
"version": "2.0.0",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"main": "dist/cli.mjs",
"bin": {
"cline": "./dist/cli.mjs"
},
"type": "module",
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"prepublishOnly": "npm run build:production",
"package:brew": "node ./scripts/update-brew-formula.mts",
"package": "npm pack --pack-destination ./dist",
"build": "node esbuild.mts",
"build:production": "node esbuild.mts --production",
"watch": "node esbuild.mts --watch",
"dev": "IS_DEV=true && npm run link && npm run watch ; npm run unlink",
"clean": "rimraf dist",
"typecheck": "tsc --noEmit",
"link": "npm run build && npm link",
"unlink": "npm unlink -g @cline/cli && npm unlink -g cline",
"test": "vitest",
"test:run": "vitest run"
},
"keywords": [
"cline",
"claude",
"dev",
"mcp",
"openrouter",
"coding",
"agent",
"autonomous",
"chatgpt",
"sonnet",
"ai",
"llama",
"cli"
],
"author": {
"name": "Cline Bot Inc."
},
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/cline/cline"
},
"homepage": "https://cline.bot",
"bugs": {
"url": "https://github.com/cline/cline/issues"
},
"devDependencies": {
"@types/node": "20.x",
"@types/prompts": "^2.4.9",
"@types/react": "^19.2.9",
"dotenv": "^16.4.5",
"esbuild": "^0.25.0",
"ink-testing-library": "^4.0.0",
"rimraf": "^6.0.1",
"typescript": "^5.4.5",
"vitest": "^4.0.17"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.13.1",
"aws4fetch": "^1.0.20",
"chalk": "^5.3.0",
"commander": "^12.1.0",
"ink": "npm:@jrichman/ink@6.4.7",
"ink-picture": "^1.3.3",
"ink-spinner": "^5.0.0",
"ora": "^8.0.1",
"nanoid": "^5.1.6",
"pino": "^10.0.0",
"pino-roll": "^4.0.0",
"prompts": "^2.4.2",
"react": "^19.2.3"
}
}
@@ -172,16 +172,6 @@ class ACPEnvServiceClient implements EnvServiceClientInterface {
Logger.debug("[ACPEnvServiceClient] shutdown called (stub)")
return proto.cline.Empty.create()
}
async openExternal(request: proto.cline.StringRequest): Promise<proto.cline.Empty> {
const url = request.value || ""
if (url) {
Logger.debug(`[ACPEnvServiceClient] openExternal: ${url}`)
const { openUrlInBrowser } = await import("../utils/browser")
await openUrlInBrowser(url)
}
return proto.cline.Empty.create()
}
}
/**
@@ -12,7 +12,11 @@
import type * as acp from "@agentclientprotocol/sdk"
import type { TerminalHandle } from "@agentclientprotocol/sdk"
import { DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT, PROCESS_HOT_TIMEOUT_NORMAL } from "@integrations/terminal/constants"
import {
DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT,
DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT,
PROCESS_HOT_TIMEOUT_NORMAL,
} from "@integrations/terminal/constants"
import type {
ITerminal,
ITerminalManager,
@@ -138,12 +142,12 @@ export interface ManagedTerminal {
* Wraps ACP terminal operations and emits events compatible with ITerminalProcess.
*/
class AcpTerminalProcess extends EventEmitter<TerminalProcessEvents> implements ITerminalProcess {
isHot = false
waitForShellIntegration = false
isHot: boolean = false
waitForShellIntegration: boolean = false
private _unretrievedOutput = ""
private _continued = false
private _completed = false
private _unretrievedOutput: string = ""
private _continued: boolean = false
private _completed: boolean = false
private _hotTimeout: NodeJS.Timeout | null = null
private _exitWaitTimeout: NodeJS.Timeout | null = null
private readonly manager: AcpTerminalManager
@@ -393,7 +397,7 @@ export class AcpTerminalManager implements ITerminalManager {
private readonly numericIdToStringId: Map<number, string> = new Map()
/** Next numeric ID to assign */
private nextNumericId = 1
private nextNumericId: number = 1
/** Active processes indexed by numeric terminal ID */
private readonly processes: Map<number, AcpTerminalProcess> = new Map()
@@ -402,8 +406,9 @@ export class AcpTerminalManager implements ITerminalManager {
private readonly terminalInfos: Map<number, TerminalInfo> = new Map()
// Configuration options for ITerminalManager
private terminalReuseEnabled = true
private terminalReuseEnabled: boolean = true
private terminalOutputLineLimit: number = DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT
private subagentTerminalOutputLineLimit: number = DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT
/**
* Creates a new AcpTerminalManager.
@@ -662,6 +667,14 @@ export class AcpTerminalManager implements ITerminalManager {
this.terminalOutputLineLimit = limit
}
/**
* Set the maximum number of output lines for subagent commands.
* @param limit Maximum number of lines
*/
setSubagentTerminalOutputLineLimit(limit: number): void {
this.subagentTerminalOutputLineLimit = limit
}
/**
* Set the default terminal profile.
* @param profile The profile identifier
@@ -674,10 +687,15 @@ export class AcpTerminalManager implements ITerminalManager {
* Process output lines, potentially truncating if over limit.
* @param outputLines Array of output lines
* @param overrideLimit Optional limit override
* @param isSubagentCommand Whether this is a subagent command
* @returns Processed output string
*/
processOutput(outputLines: string[], overrideLimit?: number): string {
const limit = overrideLimit !== undefined ? overrideLimit : this.terminalOutputLineLimit
processOutput(outputLines: string[], overrideLimit?: number, isSubagentCommand?: boolean): string {
const limit = isSubagentCommand
? overrideLimit !== undefined
? overrideLimit
: this.subagentTerminalOutputLineLimit
: this.terminalOutputLineLimit
if (outputLines.length > limit) {
const halfLimit = Math.floor(limit / 2)
@@ -173,7 +173,7 @@ export class ClineAgent implements acp.Agent {
async initialize(params: acp.InitializeRequest, connection?: acp.AgentSideConnection): Promise<acp.InitializeResponse> {
this.clientCapabilities = params.clientCapabilities
this.initializeHostProvider(this.clientCapabilities, connection)
await ClineEndpoint.initialize(this.ctx.EXTENSION_DIR)
await ClineEndpoint.initialize()
await StateManager.initialize(this.ctx.extensionContext)
return {
@@ -246,8 +246,8 @@ export class ClineAgent implements acp.Agent {
},
hostBridgeClientProvider,
(message: string) => Logger.info(message),
async (path: string) => {
return AuthHandler.getInstance().getCallbackUrl(path)
async () => {
return AuthHandler.getInstance().getCallbackUrl()
},
async () => "", // get binary location not needed in ACP mode
this.ctx.EXTENSION_DIR,
@@ -333,15 +333,14 @@ export class ClineAgent implements acp.Agent {
// Get current provider and model for the mode
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
const currentProvider = stateManager.getGlobalSettingsKey(providerKey) as ApiProvider | undefined
const modelKey = mode === "act" ? "actModeApiModelId" : "planModeApiModelId"
// Use provider-specific model ID key (e.g., cline uses actModeOpenRouterModelId)
const modelKey = currentProvider ? getProviderModelIdKey(currentProvider, mode) : null
const currentModelId = modelKey ? stateManager.getGlobalSettingsKey(modelKey) : undefined
const currentProvider = stateManager.getGlobalSettingsKey(providerKey) as ApiProvider | undefined
const currentModelId = stateManager.getGlobalSettingsKey(modelKey) as string | undefined
// Build the current model ID in provider/model format
const currentFullModelId =
currentProvider && currentModelId ? `${currentProvider}/${currentModelId}` : currentProvider || ""
currentProvider && currentModelId ? `${currentProvider}/${currentModelId}` : currentProvider || "anthropic"
// Get available models based on provider
let modelIds: string[] = []
@@ -399,11 +398,13 @@ export class ClineAgent implements acp.Agent {
const stateManager = StateManager.get()
// Update provider for both modes
// Update model for both plan and act modes (use the same model for both)
stateManager.setGlobalState("actModeApiProvider", provider)
stateManager.setGlobalState("actModeApiModelId", modelId)
stateManager.setGlobalState("planModeApiProvider", provider)
stateManager.setGlobalState("planModeApiModelId", modelId)
// Update model ID using provider-specific keys (e.g., cline uses actModeOpenRouterModelId)
// Also update the provider-specific model ID keys for both modes
const actProviderModelKey = getProviderModelIdKey(provider, "act")
if (actProviderModelKey) {
stateManager.setGlobalState(actProviderModelKey, modelId)
@@ -973,7 +974,7 @@ export class ClineAgent implements acp.Agent {
// Get the callback URL first to ensure the server is ready
let callbackUrl: string
try {
callbackUrl = await authHandler.getCallbackUrl("/auth")
callbackUrl = await authHandler.getCallbackUrl()
Logger.debug("[ClineAgent] Callback URL ready:", callbackUrl)
} catch (error) {
Logger.error("[ClineAgent] Failed to get callback URL:", error)
@@ -1158,8 +1159,8 @@ export class ClineAgent implements acp.Agent {
if (currentProvider === "cline") {
// For Cline provider, check if we have stored auth data
const values = await Promise.all(["clineApiKey", "clineAccountId"].map((key) => secretStorage.get(key)))
return values.some(Boolean)
const authData = await secretStorage.get("cline:clineAccountId")
return !!authData
}
// For OpenAI Codex provider, check OAuth credentials
@@ -1218,11 +1219,8 @@ export class ClineAgent implements acp.Agent {
const stateManager = StateManager.get()
stateManager.setGlobalState("actModeApiProvider", "openai-codex")
stateManager.setGlobalState("planModeApiProvider", "openai-codex")
// Use provider-specific model ID keys for consistency
const actModelKey = getProviderModelIdKey("openai-codex", "act")
const planModelKey = getProviderModelIdKey("openai-codex", "plan")
if (actModelKey) stateManager.setGlobalState(actModelKey, openAiCodexDefaultModelId)
if (planModelKey) stateManager.setGlobalState(planModelKey, openAiCodexDefaultModelId)
stateManager.setGlobalState("actModeApiModelId", openAiCodexDefaultModelId)
stateManager.setGlobalState("planModeApiModelId", openAiCodexDefaultModelId)
await stateManager.flushPendingState()
return {}
@@ -312,10 +312,6 @@ function translateSayMessage(
// API request finished - no specific update needed
break
case "subagent_usage":
// Hidden aggregate metrics event used for task-level accounting.
break
case "task":
// Task started - don't echo the user's prompt back to them
// The ACP client already knows what they typed
@@ -131,9 +131,9 @@ const BUTTON_CONFIGS: Record<string, ButtonConfig> = {
sendingDisabled: false,
enableButtons: true,
primaryText: "Start New Task",
secondaryText: "Exit",
secondaryText: undefined,
primaryAction: "new_task",
secondaryAction: "reject",
secondaryAction: undefined,
},
resume_task: {
sendingDisabled: false,
@@ -155,9 +155,9 @@ const BUTTON_CONFIGS: Record<string, ButtonConfig> = {
sendingDisabled: false,
enableButtons: true,
primaryText: "Start New Task with Context",
secondaryText: "Exit",
secondaryText: undefined,
primaryAction: "new_task",
secondaryAction: "reject",
secondaryAction: undefined,
},
// Streaming/partial states
@@ -82,8 +82,9 @@ describe("App", () => {
})
it("should render AuthView when view is auth", () => {
const { lastFrame } = render(<App controller={mockController} view="auth" />)
const { lastFrame } = render(<App authQuickSetup={{ provider: "openai" }} controller={mockController} view="auth" />)
expect(lastFrame()).toContain("AuthView")
expect(lastFrame()).toContain("openai")
})
it("should render ChatView when view is welcome", () => {
@@ -81,6 +81,13 @@ interface AppProps {
globalSkills?: SkillInfo[]
localSkills?: SkillInfo[]
onToggleSkill?: (isGlobal: boolean, skillPath: string, enabled: boolean) => void
// For auth view
authQuickSetup?: {
provider?: string
apikey?: string
modelid?: string
baseurl?: string
}
// For welcome view
onWelcomeSubmit?: (prompt: string, imagePaths: string[]) => void
onWelcomeExit?: () => void
@@ -88,6 +95,8 @@ interface AppProps {
initialImages?: string[]
// Stdin support
isRawModeSupported?: boolean
// Robot position (calculated before Ink mounts)
robotTopRow?: number
}
export const App: React.FC<AppProps> = ({
@@ -126,11 +135,13 @@ export const App: React.FC<AppProps> = ({
globalSkills,
localSkills,
onToggleSkill,
authQuickSetup,
onWelcomeSubmit,
onWelcomeExit,
initialPrompt,
initialImages,
isRawModeSupported = true,
robotTopRow,
}) => {
const { resizeKey } = useTerminalSize()
const [currentView, setCurrentView] = useState<ViewType>(initialView)
@@ -229,6 +240,7 @@ export const App: React.FC<AppProps> = ({
onComplete={onComplete}
onError={onError}
onNavigateToWelcome={handleNavigateToWelcome}
quickSetup={authQuickSetup}
/>
)
break
@@ -247,6 +259,7 @@ export const App: React.FC<AppProps> = ({
onComplete={onComplete}
onError={onError}
onExit={onWelcomeExit}
robotTopRow={robotTopRow}
taskId={selectedTaskId}
/>
)}
@@ -34,7 +34,8 @@ type AsciiMotionCliProps = {
autoPlay?: boolean;
loop?: boolean;
onReady?: (api: PlaybackAPI) => void;
onInteraction?: () => void; // Called when user scrolls, clicks, or drags
robotTopRow?: number; // Screen row where robot is rendered (calculated before Ink mounts)
onScroll?: () => void; // Called when user scrolls (scroll wheel)
};
const FRAMES: FrameData[] = [
@@ -333364,12 +333365,12 @@ const FRAME_BOTTOM_RIGHT = 128;
export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
hasDarkBackground = true,
onInteraction,
robotTopRow,
onScroll,
}) => {
const [frameIndex, setFrameIndex] = useState(0);
const [targetFrame, setTargetFrame] = useState(0);
const [cursor, setCursor] = useState({ x: 0, y: 0 });
const lastCursorUpdateRef = useRef(0);
const { stdout } = useStdout();
const { stdin, setRawMode } = useStdin();
@@ -333378,9 +333379,8 @@ export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
const faceX = Math.floor(terminalWidth / 2);
// Robot height after cropping is ~12 rows, eyes are roughly halfway down
const robotHeight = 12;
// Robot is always rendered at the top of the terminal in welcome state.
// faceY is the robot's eye level (row 2 + half robot height)
const faceY = 2 + Math.floor(robotHeight / 2);
// faceY is the robot's eye level - calculated from robotTopRow prop (set before Ink mounts)
const faceY = robotTopRow !== undefined ? robotTopRow + Math.floor(robotHeight / 2) : null;
// Select color theme based on background
const theme = useMemo(() => hasDarkBackground ? THEME_DARK : THEME_LIGHT, [hasDarkBackground]);
@@ -333390,13 +333390,13 @@ export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
// Stop animation on terminal resize to prevent visual glitches
useEffect(() => {
const handleResize = () => {
onInteraction?.();
onScroll?.();
};
process.stdout.on("resize", handleResize);
return () => {
process.stdout.off("resize", handleResize);
};
}, [onInteraction]);
}, [onScroll]);
// Mouse tracking - gracefully handle environments without tty support
useEffect(() => {
@@ -333417,26 +333417,15 @@ export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
const handleData = (data: Buffer) => {
const str = data.toString();
// Parse mouse events: \x1b[<button;x;yM (M=press, m=release)
// Parse mouse events: \x1b[<button;x;yM
const mouseMatch = str.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
if (mouseMatch) {
const button = parseInt(mouseMatch[1], 10);
const isPress = mouseMatch[4] === "M";
// Button 64/65 = scroll up/down
// Button 0-2 = left/middle/right click (on press)
// Button 32-34 = drag with left/middle/right button held
const isScroll = button === 64 || button === 65;
const isClick = isPress && button >= 0 && button <= 2;
const isDrag = button >= 32 && button <= 34;
if (isScroll || isClick || isDrag) {
onInteraction?.();
}
// Throttle cursor updates to ~20fps to reduce re-renders
const now = Date.now();
if (now - lastCursorUpdateRef.current >= 50) {
lastCursorUpdateRef.current = now;
setCursor({ x: parseInt(mouseMatch[2], 10), y: parseInt(mouseMatch[3], 10) });
// Button 64 = scroll up, 65 = scroll down
if (button === 64 || button === 65) {
onScroll?.();
}
setCursor({ x: parseInt(mouseMatch[2], 10), y: parseInt(mouseMatch[3], 10) });
}
};
@@ -333455,6 +333444,9 @@ export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
// Map cursor position to target frame (continuous interpolation)
useEffect(() => {
// Wait until we know where the robot is on screen
if (faceY === null) return;
const dx = cursor.x - faceX;
const dy = cursor.y - faceY;
@@ -333577,8 +333569,7 @@ export const StaticRobotFrame: React.FC<{ hasDarkBackground?: boolean }> = ({
const y = minY + originalIndex;
const croppedRow = row.slice(minX, maxX + 1);
// Manual centering for Static component compatibility
// Use Math.round to match Ink's flexbox centering behavior
const padding = Math.max(0, Math.round(((process.stdout.columns || 80) - robotWidth) / 2));
const padding = Math.max(0, Math.floor(((process.stdout.columns || 80) - robotWidth) / 2));
return (
<Box key={y}>
<Text>{" ".repeat(padding)}</Text>
@@ -3,7 +3,7 @@
* Handles different types of user interactions (text input, confirmations, choices)
*/
import type { ClineAsk, ClineMessage } from "@shared/ExtensionMessage"
import type { ClineAsk } from "@shared/ExtensionMessage"
import { Box, Text, useApp, useInput } from "ink"
import React, { useCallback, useEffect, useRef, useState } from "react"
import { useStdinContext } from "../context/StdinContext"
@@ -11,6 +11,7 @@ import { useTaskController } from "../context/TaskContext"
import { useLastCompletedAskMessage } from "../hooks/useStateSubscriber"
import { isMouseEscapeSequence } from "../utils/input"
import { jsonParseSafe } from "../utils/parser"
import { getCliMessagePrefixIcon } from "./MessageRow"
interface AskPromptProps {
onRespond?: (response: string) => void
@@ -371,73 +372,3 @@ export const AskPrompt: React.FC<AskPromptProps> = ({ onRespond }) => {
return null
}
}
/**
* Get emoji icon for message type
*/
function getCliMessagePrefixIcon(message: ClineMessage): string {
if (message.type === "ask") {
switch (message.ask) {
case "followup":
return "❓"
case "command":
case "command_output":
return "⚙️"
case "tool":
return "🔧"
case "completion_result":
return "✅"
case "api_req_failed":
return "❌"
case "resume_task":
case "resume_completed_task":
return "▶️"
case "browser_action_launch":
return "🌐"
case "use_mcp_server":
return "🔌"
case "plan_mode_respond":
return "📋"
default:
return "❔"
}
} else {
switch (message.say) {
case "task":
return "📋"
case "error":
return "❌"
case "text":
return "💬"
case "reasoning":
return "🧠"
case "completion_result":
return "✅"
case "user_feedback":
return "👤"
case "command":
case "command_output":
return "⚙️"
case "tool":
return "🔧"
case "browser_action":
case "browser_action_launch":
case "browser_action_result":
return "🌐"
case "mcp_server_request_started":
case "mcp_server_response":
return "🔌"
case "api_req_started":
case "api_req_finished":
return "🔄"
case "checkpoint_created":
return "💾"
case "info":
return "️"
case "generate_explanation":
return "📝"
default:
return " "
}
}
}
@@ -6,34 +6,24 @@
import { Box, Text, useApp, useInput } from "ink"
import Spinner from "ink-spinner"
import React, { useCallback, useEffect, useMemo, useState } from "react"
import { refreshOcaModels } from "@/core/controller/models/refreshOcaModels"
import { StateManager } from "@/core/storage/StateManager"
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
import { AuthService } from "@/services/auth/AuthService"
import { openAiCodexDefaultModelId, openRouterDefaultModelId } from "@/shared/api"
import { StringRequest } from "@/shared/proto/cline/common"
import { API_PROVIDERS_LIST, openAiCodexDefaultModelId, openRouterDefaultModelId } from "@/shared/api"
import { ProviderToApiKeyMap } from "@/shared/storage"
import { openExternal } from "@/utils/env"
import { COLORS } from "../constants/colors"
import { getAllFeaturedModels } from "../constants/featured-models"
import { useStdinContext } from "../context/StdinContext"
import { useOcaAuth } from "../hooks/useOcaAuth"
import { useScrollableList } from "../hooks/useScrollableList"
import { type DetectedSources, detectImportSources, type ImportSource } from "../utils/import-configs"
import { isMouseEscapeSequence } from "../utils/input"
import { applyBedrockConfig, applyProviderConfig } from "../utils/provider-config"
import { useValidProviders } from "../utils/providers"
import { ApiKeyInput } from "./ApiKeyInput"
import { StaticRobotFrame } from "./AsciiMotionCli"
import { type BedrockConfig, BedrockSetup } from "./BedrockSetup"
import {
FeaturedModelPicker,
getFeaturedModelAtIndex,
getFeaturedModelMaxIndex,
isBrowseAllSelected,
} from "./FeaturedModelPicker"
import { ImportView } from "./ImportView"
import { getDefaultModelId, hasModelPicker, ModelPicker } from "./ModelPicker"
import { OcaEmployeeCheck } from "./OcaEmployeeCheck"
import { getProviderLabel } from "./ProviderPicker"
import { getProviderLabel, getProviderOrder } from "./ProviderPicker"
type AuthStep =
| "menu"
@@ -45,18 +35,26 @@ type AuthStep =
| "success"
| "error"
| "cline_auth"
| "oca_employee_check"
| "oca_auth"
| "cline_model"
| "openai_codex_auth"
| "bedrock"
| "import"
// Featured models loaded from shared constants
const featuredModels = getAllFeaturedModels()
interface AuthViewProps {
controller: any
onComplete?: () => void
onError?: () => void
onNavigateToWelcome?: () => void
// Quick setup options
quickSetup?: {
provider?: string
apikey?: string
modelid?: string
baseurl?: string
}
}
interface SelectItem {
@@ -148,12 +146,9 @@ const TextInput: React.FC<{
)
}
export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onError, onNavigateToWelcome }) => {
export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onError, onNavigateToWelcome, quickSetup }) => {
const { exit } = useApp()
const providers = useValidProviders()
const [step, setStep] = useState<AuthStep>("menu")
const [step, setStep] = useState<AuthStep>(quickSetup ? "saving" : "menu")
const [selectedProvider, setSelectedProvider] = useState<string>(
StateManager.get().getApiConfiguration().actModeApiProvider ||
StateManager.get().getApiConfiguration().planModeApiProvider ||
@@ -163,6 +158,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
const [modelId, setModelId] = useState("")
const [baseUrl, setBaseUrl] = useState("")
const [errorMessage, setErrorMessage] = useState("")
const [authStatus, setAuthStatus] = useState<string>("")
const [providerSearch, setProviderSearch] = useState("")
const [providerIndex, setProviderIndex] = useState(0)
const [clineModelIndex, setClineModelIndex] = useState(0)
@@ -170,35 +166,15 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
const [importSource, setImportSource] = useState<ImportSource | null>(null)
const [bedrockConfig, setBedrockConfig] = useState<BedrockConfig | null>(null)
// OCA auth hook - enabled when step is oca_auth
const handleOcaAuthSuccess = useCallback(async () => {
await applyProviderConfig({ providerId: "oca", controller })
// Fetch OCA models from the API - this sets actModeOcaModelId/planModeOcaModelId in state
await refreshOcaModels(controller, StringRequest.create({ value: "" }))
const stateManager = StateManager.get()
stateManager.setGlobalState("welcomeViewCompleted", true)
await stateManager.flushPendingState()
setSelectedProvider("oca")
const actModelId = stateManager.getGlobalSettingsKey("actModeOcaModelId") || ""
setModelId(actModelId)
setStep("success")
}, [controller])
const handleOcaAuthError = useCallback((error: Error) => {
setErrorMessage(error.message)
setStep("error")
// Use providers.json order, filtered to only available providers
const sortedProviders = useMemo(() => {
const availableProviders = new Set(API_PROVIDERS_LIST)
return getProviderOrder().filter((p) => availableProviders.has(p))
}, [])
const { startAuth: initiateOcaAuth } = useOcaAuth({
controller,
enabled: step === "oca_auth",
onSuccess: handleOcaAuthSuccess,
onError: handleOcaAuthError,
})
// Main menu items - conditionally include import options
const mainMenuItems: SelectItem[] = useMemo(() => {
const items: SelectItem[] = [{ label: "Sign in with Cline", value: "cline_auth" }]
const items: SelectItem[] = [{ label: "Sign in with Cline account", value: "cline_auth" }]
// Add OpenAI Codex option for ChatGPT subscribers
items.push({ label: "Sign in with ChatGPT Subscription", value: "openai_codex_auth" })
@@ -221,13 +197,15 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
const providerItems: SelectItem[] = useMemo(() => {
const search = providerSearch.toLowerCase()
const filtered = providerSearch
? providers.filter((p) => p.toLowerCase().includes(search) || getProviderLabel(p).toLowerCase().includes(search))
: providers
? sortedProviders.filter(
(p) => p.toLowerCase().includes(search) || getProviderLabel(p).toLowerCase().includes(search),
)
: sortedProviders
return filtered.map((p: string) => ({
label: getProviderLabel(p),
value: p,
}))
}, [providers, providerSearch])
}, [sortedProviders, providerSearch])
// Use shared scrollable list hook for provider windowing
const TOTAL_PROVIDER_ROWS = 8
@@ -259,6 +237,13 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
}
}, [step, selectedProvider])
// Handle quick setup
useEffect(() => {
if (quickSetup && step === "saving") {
handleQuickSetup()
}
}, [quickSetup, step])
// Subscribe to auth status updates when in cline_auth step
useEffect(() => {
if (step !== "cline_auth") {
@@ -274,10 +259,21 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
}
if (authState.user && authState.user.email) {
// Auth succeeded - save configuration and transition to model selection
await applyProviderConfig({ providerId: "cline", controller })
// Auth succeeded - save configuration and transition to success
const stateManager = StateManager.get()
const mode = stateManager.getGlobalSettingsKey("mode") || "act"
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
const modelIdKey = mode === "act" ? "actModeApiModelId" : "planModeApiModelId"
const config: Record<string, string> = {
actModeApiProvider: "cline",
[providerKey]: "cline",
[modelIdKey]: openRouterDefaultModelId,
}
stateManager.setApiConfiguration(config)
stateManager.flushPendingState()
setSelectedProvider("cline")
setModelId(openRouterDefaultModelId)
setModelId(config[modelIdKey])
setStep("cline_model")
}
}
@@ -291,6 +287,80 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
}
}, [step, controller])
const handleQuickSetup = async () => {
if (!quickSetup) {
return
}
try {
const { provider, apikey, modelid, baseurl } = quickSetup
// Validate required parameters
if (!provider || !apikey || !modelid) {
setErrorMessage("Quick setup requires --provider, --apikey, and --modelid flags")
setStep("error")
return
}
const normalizedProvider = provider.toLowerCase().trim()
if (!sortedProviders.includes(normalizedProvider)) {
setErrorMessage(`Invalid provider '${provider}'. Supported providers: ${sortedProviders.join(", ")}`)
setStep("error")
return
}
if (normalizedProvider === "bedrock") {
setErrorMessage(
"Bedrock provider is not supported for quick setup due to complex authentication requirements. Please use interactive setup.",
)
setStep("error")
return
}
if (baseurl && !["openai", "openai-native"].includes(normalizedProvider)) {
setErrorMessage("Base URL is only supported for OpenAI and OpenAI-compatible providers")
setStep("error")
return
}
// Save configuration
const stateManager = StateManager.get()
const config: Record<string, string> = {
actModeApiProvider: normalizedProvider,
planModeApiProvider: normalizedProvider,
actModeApiModelId: modelid,
planModeApiModelId: modelid,
}
// Use provider-specific API key field
const keyField = ProviderToApiKeyMap[normalizedProvider]
if (keyField) {
const fields = Array.isArray(keyField) ? keyField : [keyField]
// Set the first key field for the provider
config[fields[0]] = apikey
} else {
// Fallback to generic apiKey
config.apiKey = apikey
}
if (baseurl) {
config.openAiBaseUrl = baseurl
}
stateManager.setApiConfiguration(config)
await stateManager.flushPendingState()
setSelectedProvider(normalizedProvider)
setModelId(modelid)
setBaseUrl(baseurl || "")
setStep("success")
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : String(error))
setStep("error")
}
}
// Start OpenAI Codex OAuth flow
const startOpenAiCodexAuth = useCallback(async () => {
try {
@@ -304,10 +374,19 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
await openAiCodexOAuthManager.waitForCallback()
// Success - save configuration
await applyProviderConfig({ providerId: "openai-codex", controller })
const stateManager = StateManager.get()
stateManager.setGlobalState("welcomeViewCompleted", true)
const mode = stateManager.getGlobalSettingsKey("mode") || "act"
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
const modelIdKey = mode === "act" ? "actModeApiModelId" : "planModeApiModelId"
const config: Record<string, string> = {
actModeApiProvider: "openai-codex",
planModeApiProvider: "openai-codex",
[providerKey]: "openai-codex",
[modelIdKey]: openAiCodexDefaultModelId,
}
stateManager.setApiConfiguration(config)
await stateManager.flushPendingState()
setSelectedProvider("openai-codex")
setModelId(openAiCodexDefaultModelId)
setStep("success")
@@ -322,6 +401,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
const startClineAuth = useCallback(async () => {
try {
setStep("cline_auth")
setAuthStatus("Starting authentication...")
await AuthService.getInstance(controller).createAuthRequest()
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : String(error))
@@ -329,11 +409,6 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
}
}, [controller])
const startOcaAuth = useCallback(() => {
setStep("oca_auth")
initiateOcaAuth()
}, [initiateOcaAuth])
const handleMainMenuSelect = useCallback(
(value: string) => {
if (value === "exit") {
@@ -360,9 +435,8 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
const handleProviderSelect = useCallback(
(value: string) => {
setSelectedProvider(value)
if (value === "oca") {
// Show employee check screen before starting auth
setStep("oca_employee_check")
if (value === "cline") {
startClineAuth()
} else if (value === "openai-codex") {
setStep("openai_codex_auth")
startOpenAiCodexAuth()
@@ -372,7 +446,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
setStep("apikey")
}
},
[startOcaAuth, startOpenAiCodexAuth],
[startClineAuth, startOpenAiCodexAuth],
)
const handleApiKeySubmit = useCallback(
@@ -392,24 +466,39 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
const saveConfiguration = useCallback(
async (model: string, base: string) => {
try {
if (selectedProvider === "bedrock" && bedrockConfig) {
await applyBedrockConfig({
bedrockConfig,
modelId: model,
controller,
})
} else {
await applyProviderConfig({
providerId: selectedProvider,
apiKey,
modelId: model,
baseUrl: base,
controller,
})
const stateManager = StateManager.get()
const config: Record<string, string> = {
actModeApiProvider: selectedProvider,
planModeApiProvider: selectedProvider,
actModeApiModelId: model,
planModeApiModelId: model,
apiProvider: selectedProvider,
}
const stateManager = StateManager.get()
stateManager.setGlobalState("welcomeViewCompleted", true)
// Add API key or Bedrock-specific config
if (selectedProvider === "bedrock" && bedrockConfig) {
const bedrockFields: Record<string, unknown> = {
awsAuthentication: bedrockConfig.awsAuthentication,
awsRegion: bedrockConfig.awsRegion,
awsUseCrossRegionInference: bedrockConfig.awsUseCrossRegionInference,
}
if (bedrockConfig.awsProfile !== undefined) bedrockFields.awsProfile = bedrockConfig.awsProfile
if (bedrockConfig.awsAccessKey) bedrockFields.awsAccessKey = bedrockConfig.awsAccessKey
if (bedrockConfig.awsSecretKey) bedrockFields.awsSecretKey = bedrockConfig.awsSecretKey
if (bedrockConfig.awsSessionToken) bedrockFields.awsSessionToken = bedrockConfig.awsSessionToken
Object.assign(config, bedrockFields)
} else if (apiKey) {
const keyField = ProviderToApiKeyMap[selectedProvider as keyof typeof ProviderToApiKeyMap]
if (keyField) {
const fields = Array.isArray(keyField) ? keyField : [keyField]
config[fields[0]] = apiKey
}
}
if (base) {
config.openAiBaseUrl = base
}
stateManager.setApiConfiguration(config)
await stateManager.flushPendingState()
setStep("success")
@@ -418,7 +507,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
setStep("error")
}
},
[selectedProvider, apiKey, bedrockConfig, controller],
[selectedProvider, apiKey, bedrockConfig],
)
const handleModelIdSubmit = useCallback(
@@ -470,18 +559,11 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
}, [])
// Auto-navigate to welcome after success (immediate)
// For quick setup mode (no onNavigateToWelcome), exit the Ink app
useEffect(() => {
if (step === "success") {
if (onNavigateToWelcome) {
onNavigateToWelcome()
} else {
// Quick setup mode - exit Ink app after successful configuration
// The cleanup handler in runInkApp will handle process exit
exit()
}
if (step === "success" && onNavigateToWelcome) {
onNavigateToWelcome()
}
}, [step, onNavigateToWelcome, exit])
}, [step, onNavigateToWelcome])
// Error screen menu items
const errorMenuItems: SelectItem[] = useMemo(() => {
@@ -538,12 +620,6 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
setBaseUrl("")
setStep("modelid")
break
case "oca_employee_check":
setStep("provider")
break
case "oca_auth":
setStep("oca_employee_check")
break
case "cline_auth":
setStep("menu")
break
@@ -682,10 +758,6 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
</Box>
)
case "oca_employee_check":
return <OcaEmployeeCheck isActive={step === "oca_employee_check"} onCancel={goBack} onSignIn={startOcaAuth} />
case "oca_auth":
case "cline_auth":
return (
<Box flexDirection="column">
@@ -720,11 +792,50 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
)
case "cline_model": {
const allModels = featuredModels
return (
<Box flexDirection="column">
<Text color="white">Choose a model</Text>
<Text> </Text>
<FeaturedModelPicker selectedIndex={clineModelIndex} />
{/* Model list */}
{allModels.map((model, i) => (
<Box flexDirection="column" key={model.id} marginBottom={1}>
<Box>
<Text color={i === clineModelIndex ? COLORS.primaryBlue : undefined}>
{i === clineModelIndex ? " " : " "}
</Text>
<Text bold color={i === clineModelIndex ? COLORS.primaryBlue : "white"}>
{model.name}
</Text>
{model.label && (
<>
<Text> </Text>
<Text
backgroundColor={model.label === "FREE" ? "gray" : COLORS.primaryBlue}
color="black">
{" "}
{model.label}{" "}
</Text>
</>
)}
</Box>
<Box paddingLeft={2}>
<Text color="gray">{model.description}</Text>
</Box>
</Box>
))}
{/* Browse all option */}
<Box>
<Text color={clineModelIndex === allModels.length ? COLORS.primaryBlue : "gray"}>
{clineModelIndex === allModels.length ? " " : " "}
Browse all models...
</Text>
</Box>
<Text> </Text>
<Text color="gray">Arrows to navigate, Enter to select</Text>
</Box>
)
}
@@ -770,13 +881,11 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
const [menuIndex, setMenuIndex] = useState(0)
// Steps that allow going back with escape (apikey handled by ApiKeyInput component)
// OcaEmployeeCheck handles its own escape key, so oca_employee_check is not in this list
const canGoBack = [
"provider",
"modelid",
"baseurl",
"cline_auth",
"oca_auth",
"cline_model",
"openai_codex_auth",
"bedrock",
@@ -814,20 +923,20 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
setProviderSearch((prev) => prev + input)
}
} else if (step === "cline_model") {
const maxIndex = getFeaturedModelMaxIndex()
const allModels = featuredModels
const maxIndex = allModels.length // includes "Browse all" option
if (key.upArrow) {
setClineModelIndex((prev) => (prev > 0 ? prev - 1 : maxIndex))
} else if (key.downArrow) {
setClineModelIndex((prev) => (prev < maxIndex ? prev + 1 : 0))
} else if (key.return) {
if (isBrowseAllSelected(clineModelIndex)) {
if (clineModelIndex === allModels.length) {
// "Browse all models" selected
setStep("modelid")
} else {
const selectedModel = getFeaturedModelAtIndex(clineModelIndex)
if (selectedModel) {
handleClineModelSelect(selectedModel.id)
}
// Featured model selected
handleClineModelSelect(allModels[clineModelIndex].id)
}
}
}
@@ -864,12 +973,9 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
<Text> </Text>
{mainMenuItems.map((item, index) => (
<Box key={item.value}>
<Text>
<Text color={index === menuIndex ? COLORS.primaryBlue : undefined}>
{index === menuIndex ? " " : " "}
{item.label}
</Text>
{item.value === "cline_auth" && <Text color="yellow"> (try Opus 4.6!)</Text>}
<Text color={index === menuIndex ? COLORS.primaryBlue : undefined}>
{index === menuIndex ? " " : " "}
{item.label}
</Text>
</Box>
))}
@@ -8,82 +8,40 @@
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@shared/ClineAccount"
import { COMMAND_OUTPUT_STRING } from "@shared/combineCommandSequences"
import type { ClineAskUseMcpServer, ClineMessage } from "@shared/ExtensionMessage"
import type { ClineMessage } from "@shared/ExtensionMessage"
import { Box, Text } from "ink"
import Spinner from "ink-spinner"
import React from "react"
import { COLORS } from "../constants/colors"
import { useTerminalSize } from "../hooks/useTerminalSize"
import { jsonParseSafe } from "../utils/parser"
import { getToolDescription, isFileEditTool, parseToolFromMessage } from "../utils/tools"
import { DiffView } from "./DiffView"
import { SubagentMessage } from "./SubagentMessage"
/**
* Add "(Tab)" hint after "Act mode" mentions.
* Case-insensitive, avoids double-adding if already present.
* Matches just "Act mode" without requiring "to " prefix because markdown
* processing may split "toggle to **Act mode**" into separate text chunks.
*/
function addActModeHint(text: string, keyPrefix: string): React.ReactNode[] {
// Match "Act mode" in various capitalizations, but not if already followed by (Tab)
const actModeRegex = /\bact\s+mode\b(?!\s*\(tab\))/gi
const parts = text.split(actModeRegex)
const matches = text.match(actModeRegex)
if (!matches || parts.length <= 1) {
return [text]
}
const nodes: React.ReactNode[] = []
parts.forEach((part, i) => {
if (part) {
nodes.push(part)
}
if (matches[i]) {
nodes.push(
<React.Fragment key={`${keyPrefix}-act-mode-${i}`}>
{matches[i]}
<Text color="gray"> (Tab)</Text>
</React.Fragment>,
)
}
})
return nodes
}
/**
* Render inline markdown: **bold**, *italic*, `code`
* Also adds "(Tab)" hints after "Act mode" mentions.
* Returns array of React nodes with appropriate styling
*/
function renderInlineMarkdown(text: string): React.ReactNode[] {
const nodes: React.ReactNode[] = []
let hintCallIndex = 0
const addHintedText = (value: string) => addActModeHint(value, `hint-${hintCallIndex++}`)
// Match **bold**, *italic*, or `code` - order matters (** before *)
const regex = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g
let lastIndex = 0
let match
while ((match = regex.exec(text)) !== null) {
// Add text before match (with Act Mode hint processing)
// Add text before match
if (match.index > lastIndex) {
const beforeText = text.slice(lastIndex, match.index)
nodes.push(...addHintedText(beforeText))
nodes.push(text.slice(lastIndex, match.index))
}
const fullMatch = match[0]
const key = `md-${match.index}`
if (fullMatch.startsWith("**") && fullMatch.endsWith("**")) {
// Bold - also process for Act Mode hints inside bold text
const boldContent = fullMatch.slice(2, -2)
const hintedContent = addHintedText(boldContent)
// Bold
nodes.push(
<Text bold key={key}>
{hintedContent}
{fullMatch.slice(2, -2)}
</Text>,
)
} else if (fullMatch.startsWith("*") && fullMatch.endsWith("*")) {
@@ -101,12 +59,12 @@ function renderInlineMarkdown(text: string): React.ReactNode[] {
lastIndex = regex.lastIndex
}
// Add remaining text (with Act Mode hint processing)
// Add remaining text
if (lastIndex < text.length) {
nodes.push(...addHintedText(text.slice(lastIndex)))
nodes.push(text.slice(lastIndex))
}
return nodes.length > 0 ? nodes : addHintedText(text)
return nodes.length > 0 ? nodes : [text]
}
/**
@@ -130,20 +88,10 @@ interface ChatMessageProps {
* For this to work properly, parent containers must have width="100%"
* so flexGrow={1} on the content box has a reference width to fill.
*/
const DotRow: React.FC<{ children: React.ReactNode; color?: string; flashing?: boolean }> = ({
children,
color,
flashing = false,
}) => (
const DotRow: React.FC<{ children: React.ReactNode; color?: string }> = ({ children, color }) => (
<Box flexDirection="row">
<Box width={2}>
{flashing ? (
<Text color={color}>
<Spinner type="toggle8" />
</Text>
) : (
<Text color={color}></Text>
)}
<Text color={color}></Text>
</Box>
<Box flexGrow={1}>{children}</Box>
</Box>
@@ -166,11 +114,6 @@ const ResultRow: React.FC<{ children: React.ReactNode; isFirst?: boolean }> = ({
* Get the primary argument to display for a tool (file path, command, url, etc.)
*/
function getToolMainArg(_toolName: string, args: Record<string, unknown>): string {
// Search files: show 'regex' in path
if (typeof args.regex === "string" && typeof args.path === "string") {
return `'${args.regex}' in ${args.path}`
}
// File path
if (typeof args.path === "string") return args.path
if (typeof args.file_path === "string") return args.file_path
@@ -180,6 +123,9 @@ function getToolMainArg(_toolName: string, args: Record<string, unknown>): strin
return args.command.length > 120 ? args.command.substring(0, 117) + "..." : args.command
}
// Search regex
if (typeof args.regex === "string") return args.regex
// URL
if (typeof args.url === "string") return args.url
@@ -227,7 +173,7 @@ function truncate(text: string, maxLength: number): string {
/**
* Format tool result for display
*/
function formatToolResult(result: string, maxLines = 5): string[] {
function formatToolResult(result: string, maxLines: number = 5): string[] {
const lines = result.split("\n")
if (lines.length <= maxLines) {
return lines
@@ -237,8 +183,8 @@ function formatToolResult(result: string, maxLines = 5): string[] {
return displayLines
}
export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStreaming }) => {
const { type, ask, say, text, partial } = message
export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode }) => {
const { type, ask, say, text } = message
const toolColor = mode === "plan" ? "yellow" : COLORS.primaryBlue
const { columns: terminalWidth } = useTerminalSize()
@@ -276,7 +222,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow>
<MarkdownText>{text}</MarkdownText>
<Text>{text}</Text>
</DotRow>
</Box>
)
@@ -294,26 +240,27 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
if (isFileEditTool(toolInfo.toolName) && filePath && toolInfo.args.content) {
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
<DotRow color={toolColor}>
<ToolCallText args={toolInfo.args} isAsk={isToolAsk} mode={mode} toolName={toolInfo.toolName} />
</DotRow>
<Box marginLeft={2}>
<DiffView content={toolInfo.args.content as string} filePath={filePath as string | undefined} />
<DiffView content={toolInfo.args.content} />
</Box>
</Box>
)
}
// Show result content for completed tools (both say and ask), or file path for pending asks
const contentLines = toolInfo.result?.trim()
? formatToolResult(toolInfo.result, 5)
: (isToolAsk || isToolSay) && filePath
? [filePath as string]
: []
// Show result content for completed tools, or file path for pending asks
const contentLines =
isToolSay && toolInfo.result?.trim()
? formatToolResult(toolInfo.result, 5)
: isToolAsk && filePath
? [filePath as string]
: []
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
<DotRow color={toolColor}>
<ToolCallText args={toolInfo.args} isAsk={isToolAsk} mode={mode} toolName={toolInfo.toolName} />
</DotRow>
{contentLines.length > 0 && (
@@ -332,7 +279,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
if (isToolSay) {
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
<DotRow color={toolColor}>
<Text color={toolColor}>{truncate(text, 100)}</Text>
</DotRow>
</Box>
@@ -354,7 +301,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
<DotRow color={toolColor}>
<Text>
<Text color={toolColor}>{label}</Text>
<Text>{truncate(command, 120)}</Text>
@@ -389,102 +336,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
)
}
// MCP approval (ask) or acknowledgment (say)
if ((type === "ask" && ask === "use_mcp_server") || say === "use_mcp_server") {
const isAsk = type === "ask"
const parsed = text
? jsonParseSafe<Partial<ClineAskUseMcpServer> & { serverName: string }>(text, {
type: undefined,
serverName: "unknown server",
toolName: undefined,
arguments: undefined,
uri: undefined,
})
: undefined
const serverName = parsed?.serverName || "unknown server"
const actionLabel = isAsk ? "Cline wants to use MCP" : "Cline used MCP"
const targetLine =
parsed?.type === "access_mcp_resource"
? `resource: ${parsed?.uri || "unknown"}`
: parsed?.type === "use_mcp_tool"
? `tool: ${parsed?.toolName || "unknown"}`
: "tool: unknown"
let argsLines: string[] = []
if (parsed?.arguments && parsed.arguments.trim() && parsed.arguments !== "{}") {
let formattedArgs = parsed.arguments
try {
formattedArgs = JSON.stringify(JSON.parse(parsed.arguments), null, 2)
} catch {
// Keep raw string if not valid JSON
}
argsLines = formatToolResult(formattedArgs, 10)
}
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
<Text>
<Text color={toolColor}>{actionLabel}</Text>
<Text>{`: ${serverName}`}</Text>
</Text>
</DotRow>
<Box flexDirection="column" marginLeft={2} width="100%">
<ResultRow isFirst>
<Text color="gray">{targetLine}</Text>
</ResultRow>
{argsLines.length > 0 && (
<Box flexDirection="column" paddingLeft={3} width="100%">
<Text color="gray">args:</Text>
{argsLines.map((line, idx) => (
<Text color="gray" key={`mcp-args-${idx}`}>
{line}
</Text>
))}
</Box>
)}
</Box>
</Box>
)
}
if ((type === "ask" && ask === "use_subagents") || say === "use_subagents" || say === "subagent") {
return <SubagentMessage isStreaming={isStreaming} message={message} mode={mode} />
}
// MCP response
if (say === "mcp_server_response" && text) {
const lines = formatToolResult(text, 8)
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
<Text color={toolColor}>MCP response</Text>
</DotRow>
<Box flexDirection="column" marginLeft={2} width="100%">
{lines.map((line, idx) => (
<ResultRow isFirst={idx === 0} key={idx}>
<Text color="gray">{line}</Text>
</ResultRow>
))}
</Box>
</Box>
)
}
// Error messages
if (say === "clineignore_error") {
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color="red">
<Text color="red" wrap="wrap">
Cline tried to access <Text bold>{text}</Text> which is blocked by the .clineignore file.
</Text>
</DotRow>
</Box>
)
}
if (say === "error" || (type === "ask" && ask === "api_req_failed")) {
// Try to parse error message if it's JSON
let errorMessage = text || "Unknown error"
@@ -599,7 +451,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
if (say === "browser_action" || say === "browser_action_launch") {
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
<DotRow color={toolColor}>
<Text>
<Text color={toolColor}>Cline used the browser</Text>
{text && (
@@ -618,7 +470,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
if (say === "mcp_server_request_started") {
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
<DotRow color={toolColor}>
<Text>
<Text color={toolColor}>Cline is using an MCP tool</Text>
{text && (
@@ -633,20 +485,6 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
)
}
// MCP notifications
if (say === "mcp_notification" && text) {
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color={toolColor}>
<Text>
<Text color={toolColor}>MCP Notification</Text>
<Text>: {truncate(text, 120)}</Text>
</Text>
</DotRow>
</Box>
)
}
// Info messages
if (say === "info") {
return (
@@ -688,17 +526,6 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
}
}
// Act mode response (non-blocking progress update)
if (type === "ask" && ask === "act_mode_respond" && text) {
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color={toolColor}>
<MarkdownText color={toolColor}>{text}</MarkdownText>
</DotRow>
</Box>
)
}
// Plan mode response
if (type === "ask" && ask === "plan_mode_respond" && text) {
const parsed = jsonParseSafe(text, { response: undefined as string | undefined })
@@ -713,19 +540,6 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
}
}
// Mistake limit reached (ask)
if (type === "ask" && ask === "mistake_limit_reached") {
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color="red">
<Text color="red" wrap="wrap">
<Text bold>Error</Text>: {text || "Mistake limit reached."}
</Text>
</DotRow>
</Box>
)
}
// New task request from assistant
if (type === "ask" && ask === "new_task" && text) {
return (
@@ -742,54 +556,6 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
)
}
// Condense conversation request
if (type === "ask" && ask === "condense" && text) {
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color={COLORS.primaryBlue} flashing={partial === true && isStreaming}>
<Text bold color={COLORS.primaryBlue}>
Cline wants to condense your conversation:
</Text>
</DotRow>
<Box flexDirection="column" paddingLeft={2}>
<Text color="gray">{text}</Text>
</Box>
</Box>
)
}
// Summarize task request
if (type === "ask" && ask === "summarize_task" && text) {
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color={COLORS.primaryBlue} flashing={partial === true && isStreaming}>
<Text bold color={COLORS.primaryBlue}>
Cline wants to summarize the task:
</Text>
</DotRow>
<Box flexDirection="column" paddingLeft={2}>
<Text color="gray">{text}</Text>
</Box>
</Box>
)
}
// Report bug request
if (type === "ask" && ask === "report_bug" && text) {
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color={COLORS.primaryBlue} flashing={partial === true && isStreaming}>
<Text bold color={COLORS.primaryBlue}>
Cline wants to create a Github issue:
</Text>
</DotRow>
<Box flexDirection="column" paddingLeft={2}>
<Text color="gray">{text}</Text>
</Box>
</Box>
)
}
// Skip other message types
return null
}
@@ -807,8 +573,6 @@ export const ChatMessageList: React.FC<ChatMessageListProps> = ({ messages, maxM
const displayMessages = messages.filter((m) => {
// Skip api_req_finished, they're just markers
if (m.say === "api_req_finished") return false
// Skip hidden aggregated usage messages
if (m.say === "subagent_usage") return false
// Skip empty text messages
if (m.say === "text" && !m.text?.trim()) return false
// Skip checkpoint messages
@@ -1,7 +1,7 @@
/**
* Tests for ChatView component exit and cleanup behavior
*
* These tests verify that when the user exits (via shutdown event or other means),
* These tests verify that when the user exits (via Ctrl+C or other means),
* the input field is properly hidden before the app terminates.
*/
@@ -12,6 +12,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { ChatView } from "./ChatView"
// Helper to wait for async state updates
// Using 60ms since handleExit has a 50ms setTimeout
const delay = (ms: number = 60) => new Promise((resolve) => setTimeout(resolve, ms))
// Type for our exit mock function
@@ -174,21 +175,12 @@ vi.mock("@shared/getApiMetrics", () => ({
totalTokensOut: 0,
totalCost: 0,
})),
getLastApiReqTotalTokens: vi.fn(() => 0),
}))
vi.mock("child_process", () => ({
exec: vi.fn(),
execSync: vi.fn(() => "main"),
}))
// Mock telemetry service to prevent HostProvider errors in shutdown handler
vi.mock("@/services/telemetry", () => ({
telemetryService: {
captureHostEvent: vi.fn(),
},
}))
// Helper to create a typed mock for onExit
const createExitMock = (): ExitMockFn => vi.fn() as ExitMockFn
@@ -221,6 +213,33 @@ describe("ChatView Exit and Cleanup", () => {
})
})
describe("Ctrl+C exit handling", () => {
it("should hide input but keep footer, then call onExit", async () => {
const { lastFrame, stdin } = render(<ChatView onExit={mockOnExit} />)
// Verify UI visible before Ctrl+C
expect(lastFrame()).toContain("Input:")
expect(lastFrame()).toContain("@ for files")
// Simulate Ctrl+C
stdin.write("\x03")
// onExit should not be called immediately
expect(mockOnExit).not.toHaveBeenCalled()
// Wait for state update and callback
await delay()
// Input should be hidden, but footer should remain
const frameAfter = lastFrame()
expect(frameAfter).not.toContain("Input:")
expect(frameAfter).toContain("@ for files")
// onExit should have been called
expect(mockOnExit).toHaveBeenCalledTimes(1)
})
})
describe("Shutdown event handling", () => {
it("should subscribe on mount and unsubscribe on unmount", () => {
const { unmount } = render(<ChatView onExit={mockOnExit} />)
@@ -230,59 +249,39 @@ describe("ChatView Exit and Cleanup", () => {
expect(shutdownMockState.listeners.length).toBe(0)
})
it("should hide input when shutdown event fires", async () => {
it("should hide UI when shutdown event fires", async () => {
const { lastFrame } = render(<ChatView onExit={mockOnExit} />)
// Input should be visible initially
expect(lastFrame()).toContain("Input:")
// Fire shutdown event (simulates Ctrl+C)
shutdownMockState.fire()
await delay()
// Input should be hidden after shutdown
expect(lastFrame()).not.toContain("Input:")
})
it("should preserve footer when shutdown event fires", async () => {
const { lastFrame } = render(<ChatView onExit={mockOnExit} />)
// Footer should be visible initially
expect(lastFrame()).toContain("@ for files")
// Fire shutdown event
shutdownMockState.fire()
await delay()
// Footer should still be present (only input is hidden)
expect(lastFrame()).toContain("@ for files")
})
})
describe("Edge cases", () => {
it("should handle shutdown event when onExit prop is undefined", async () => {
const { lastFrame } = render(<ChatView />)
it("should handle exit when onExit prop is undefined", async () => {
const { lastFrame, stdin } = render(<ChatView />)
// Fire shutdown event
shutdownMockState.fire()
stdin.write("\x03")
await delay()
// Should not throw, UI should still hide
expect(lastFrame()).not.toContain("Input:")
})
it("should handle multiple shutdown events gracefully", async () => {
const { lastFrame } = render(<ChatView onExit={mockOnExit} />)
it("should handle multiple Ctrl+C presses gracefully", async () => {
const { stdin } = render(<ChatView onExit={mockOnExit} />)
// Fire multiple shutdown events
shutdownMockState.fire()
shutdownMockState.fire()
shutdownMockState.fire()
stdin.write("\x03")
stdin.write("\x03")
stdin.write("\x03")
await delay()
// UI should still hide properly
expect(lastFrame()).not.toContain("Input:")
expect(mockOnExit).toHaveBeenCalled()
})
})
})
@@ -295,15 +294,14 @@ describe("ChatView UI State During Exit", () => {
it("should preserve static content and footer, only hide input during exit", async () => {
const onExit = createExitMock()
const { lastFrame } = render(<ChatView onExit={onExit} />)
const { lastFrame, stdin } = render(<ChatView onExit={onExit} />)
// Footer contains auto-approve toggle
expect(lastFrame()).toContain("Auto-approve")
expect(lastFrame()).toContain("What can I do for you?")
expect(lastFrame()).toContain("Input:")
// Fire shutdown event
shutdownMockState.fire()
stdin.write("\x03")
await delay()
const frameAfter = lastFrame()
@@ -101,19 +101,16 @@
* - log-update: node_modules/ink/build/log-update.js (eraseLines logic)
*/
import type { ApiProvider, ModelInfo } from "@shared/api"
import type { ModelInfo } from "@shared/api"
import { combineCommandSequences } from "@shared/combineCommandSequences"
import { combineHookSequences } from "@shared/combineHookSequences"
import type { ClineAsk, ClineMessage } from "@shared/ExtensionMessage"
import { getApiMetrics, getLastApiReqTotalTokens } from "@shared/getApiMetrics"
import { EmptyRequest, StringRequest } from "@shared/proto/cline/common"
import type { SlashCommandInfo } from "@shared/proto/cline/slash"
import { CLI_ONLY_COMMANDS } from "@shared/slashCommands"
import { getProviderDefaultModelId, getProviderModelIdKey } from "@shared/storage"
import type { Mode } from "@shared/storage/types"
import { execSync } from "child_process"
import { Box, Static, Text, useApp, useInput } from "ink"
// biome-ignore lint/style/useImportType: JSX requires React as a value (jsx: "react" in tsconfig)
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { getAvailableSlashCommands } from "@/core/controller/slash/getAvailableSlashCommands"
import { showTaskWithId } from "@/core/controller/task/showTaskWithId"
@@ -123,9 +120,7 @@ import { Logger } from "@/shared/services/Logger"
import { Session } from "@/shared/services/Session"
import { COLORS } from "../constants/colors"
import { useTaskContext, useTaskState } from "../context/TaskContext"
import { useHomeEndKeys } from "../hooks/useHomeEndKeys"
import { useIsSpinnerActive } from "../hooks/useStateSubscriber"
import { findWordEnd, findWordStart, useTextInput } from "../hooks/useTextInput"
import { moveCursorDown, moveCursorUp } from "../utils/cursor"
import { setTerminalTitle } from "../utils/display"
import {
@@ -138,44 +133,26 @@ import {
import { isMouseEscapeSequence } from "../utils/input"
import { jsonParseSafe, parseImagesFromInput } from "../utils/parser"
import { extractSlashQuery, filterCommands, insertSlashCommand, sortCommandsWorkflowsFirst } from "../utils/slash-commands"
import { waitFor } from "../utils/timeout"
import { isFileEditTool, parseToolFromMessage } from "../utils/tools"
import { shutdownEvent } from "../vscode-shim"
import { ActionButtons, type ButtonActionType, getButtonConfig, getVisibleButtons } from "./ActionButtons"
import { AsciiMotionCli, StaticRobotFrame } from "./AsciiMotionCli"
import { ChatMessage } from "./ChatMessage"
import { FileMentionMenu } from "./FileMentionMenu"
import { HelpPanelContent } from "./HelpPanelContent"
import { HighlightedInput } from "./HighlightedInput"
import { HistoryPanelContent } from "./HistoryPanelContent"
import { providerModels } from "./ModelPicker"
import { SessionSummary } from "./SessionSummary"
import { SettingsPanelContent } from "./SettingsPanelContent"
import { SlashCommandMenu } from "./SlashCommandMenu"
import { ThinkingIndicator } from "./ThinkingIndicator"
/**
* Persistent input storage that survives React remounts (e.g., during terminal resize).
* Keyed by a stable identifier so each task/session maintains its own input state.
*/
interface PersistedInputState {
text: string
cursorPos: number
pastedTexts: Map<number, string>
pasteCounter: number
}
const inputStateStorage = new Map<string, PersistedInputState>()
function getInputStorageKey(controller: any, taskId?: string): string {
// Use taskId if available, otherwise fall back to controller instance
return taskId || (controller?.task?.taskId ?? "default")
}
interface ChatViewProps {
controller?: any
onExit?: () => void
onComplete?: () => void
onError?: () => void
robotTopRow?: number
initialPrompt?: string
initialImages?: string[]
taskId?: string
@@ -229,9 +206,9 @@ function getGitDiffStats(cwd?: string): GitDiffStats | null {
const delMatch = output.match(/(\d+) deletion/)
return {
files: filesMatch ? Number.parseInt(filesMatch[1], 10) : 0,
additions: addMatch ? Number.parseInt(addMatch[1], 10) : 0,
deletions: delMatch ? Number.parseInt(delMatch[1], 10) : 0,
files: filesMatch ? parseInt(filesMatch[1], 10) : 0,
additions: addMatch ? parseInt(addMatch[1], 10) : 0,
deletions: delMatch ? parseInt(delMatch[1], 10) : 0,
}
} catch {
return null
@@ -242,7 +219,7 @@ function getGitDiffStats(cwd?: string): GitDiffStats | null {
* Create a progress bar for context window usage
* Returns { filled, empty } strings to allow different coloring
*/
function createContextBar(used: number, total: number, width = 8): { filled: string; empty: string } {
function createContextBar(used: number, total: number, width: number = 8): { filled: string; empty: string } {
const ratio = Math.min(used / total, 1)
// Use ceil so any usage > 0 shows at least one bar
const filledCount = used > 0 ? Math.max(1, Math.ceil(ratio * width)) : 0
@@ -332,7 +309,7 @@ function parseAskOptions(text: string): string[] {
*/
function expandPastedTexts(text: string, pastedTexts: Map<number, string>): string {
return text.replace(/\[Pasted text #(\d+) \+\d+ lines\]/g, (match, num) => {
const content = pastedTexts.get(Number.parseInt(num, 10))
const content = pastedTexts.get(parseInt(num, 10))
return content ?? match
})
}
@@ -342,6 +319,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
onExit,
onComplete: _onComplete,
onError,
robotTopRow,
initialPrompt,
initialImages,
taskId,
@@ -357,27 +335,9 @@ export const ChatView: React.FC<ChatViewProps> = ({
// Prefer prop controller over context controller (memoized for stable reference in callbacks)
const ctrl = useMemo(() => controller || taskController, [controller, taskController])
// Input state - using hook for text editing with keyboard shortcuts
const {
text: textInput,
cursorPos,
setText: setTextInput,
setCursorPos,
handleKeyboardSequence,
handleCtrlShortcut,
deleteCharBefore,
insertText: insertTextAtCursor,
} = useTextInput()
// Get storage key for persisting input across remounts
const storageKey = useMemo(() => getInputStorageKey(ctrl, taskId), [ctrl, taskId])
// Refs for text input and cursor position (used by useHomeEndKeys and to avoid stale closures in useInput)
const textInputRef = useRef(textInput)
textInputRef.current = textInput
const cursorPosRef = useRef(cursorPos)
cursorPosRef.current = cursorPos
// Input state
const [textInput, setTextInput] = useState("")
const [cursorPos, setCursorPos] = useState(0)
const [fileResults, setFileResults] = useState<FileSearchResult[]>([])
const [selectedIndex, setSelectedIndex] = useState(0) // For file menu
const [historyIndex, setHistoryIndex] = useState(-1) // -1 = not browsing history, 0+ = history item index
@@ -388,10 +348,8 @@ export const ChatView: React.FC<ChatViewProps> = ({
const [userScrolled, setUserScrolled] = useState(false)
// Pasted text storage - maps placeholder number to full pasted content
const [pastedTexts, setPastedTexts] = useState<Map<number, string>>(() => {
return inputStateStorage.get(storageKey)?.pastedTexts ?? new Map()
})
const pasteCounterRef = useRef<number>(inputStateStorage.get(storageKey)?.pasteCounter ?? 0)
const [pastedTexts, setPastedTexts] = useState<Map<number, string>>(new Map())
const pasteCounterRef = useRef(0)
// Track paste timing to combine chunks that arrive in rapid succession
const lastPasteTimeRef = useRef<number>(0)
const activePasteNumRef = useRef<number>(0)
@@ -407,47 +365,14 @@ export const ChatView: React.FC<ChatViewProps> = ({
const [slashMenuDismissed, setSlashMenuDismissed] = useState(false)
const lastSlashIndexRef = useRef<number>(-1)
// Panel state
// Settings panel state
const [activePanel, setActivePanel] = useState<
| { type: "settings"; initialMode?: "model-picker" | "featured-models"; initialModelKey?: "actModelId" | "planModelId" }
| { type: "history" }
| { type: "help" }
| null
{ type: "settings"; initialMode?: "model-picker" } | { type: "history" } | null
>(null)
// Handle Home/End keys from raw stdin (Ink doesn't expose these in useInput)
useHomeEndKeys({
onHome: useCallback(() => setCursorPos(0), [setCursorPos]),
onEnd: useCallback(() => setCursorPos(textInputRef.current.length), [setCursorPos]),
isActive: !activePanel, // Only active when no panel is open
})
// Track when we're exiting to hide UI elements before exit
const [isExiting, setIsExiting] = useState(false)
// Restore input state from storage on mount (after resize remount)
useEffect(() => {
const stored = inputStateStorage.get(storageKey)
if (stored) {
setTextInput(stored.text)
setCursorPos(stored.cursorPos)
setPastedTexts(stored.pastedTexts)
pasteCounterRef.current = stored.pasteCounter
}
}, [storageKey, setTextInput, setCursorPos])
// Persist input state to storage whenever it changes (survives remount)
useEffect(() => {
if (textInput || pastedTexts.size > 0) {
inputStateStorage.set(storageKey, {
text: textInput,
cursorPos,
pastedTexts: new Map(pastedTexts),
pasteCounter: pasteCounterRef.current,
})
}
}, [storageKey, textInput, cursorPos, pastedTexts])
// Task switch handling: when switching tasks via /history, we clear the terminal and
// increment a counter used as the root Box's key. This forces React to remount the tree,
// giving us a fresh Static instance. Mirrors how App.tsx handles resize with resizeKey.
@@ -474,10 +399,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
return stateManager.getGlobalSettingsKey("mode") || "act"
})
const [yolo, _setYolo] = useState<boolean>(() => StateManager.get().getGlobalSettingsKey("yoloModeToggled") ?? false)
const [autoApproveAll, setAutoApproveAll] = useState<boolean>(
() => StateManager.get().getGlobalSettingsKey("autoApproveAllToggled") ?? false,
)
const [yolo, setYolo] = useState<boolean>(() => StateManager.get().getGlobalSettingsKey("yoloModeToggled") ?? false)
// Sync mode from core state updates (e.g. yolo auto-switching plan to act)
useEffect(() => {
@@ -486,30 +408,27 @@ export const ChatView: React.FC<ChatViewProps> = ({
}
}, [taskState.mode])
const toggleAutoApproveAll = useCallback(() => {
const newValue = !autoApproveAll
setAutoApproveAll(newValue)
StateManager.get().setGlobalState("autoApproveAllToggled", newValue)
}, [autoApproveAll])
const toggleYolo = useCallback(() => {
const newValue = !yolo
setYolo(newValue)
StateManager.get().setGlobalState("yoloModeToggled", newValue)
}, [yolo])
// Get provider based on current mode (computed first since modelId depends on it)
// Get model ID based on current mode
// Re-read when activePanel changes (settings panel closes) to pick up changes
const modelId = useMemo(() => {
const stateManager = StateManager.get()
const modelKey = mode === "act" ? "actModeApiModelId" : "planModeApiModelId"
return (stateManager.getGlobalSettingsKey(modelKey) as string) || "claude-sonnet-4-20250514"
}, [mode, activePanel])
// Get provider based on current mode
const provider = useMemo(() => {
const stateManager = StateManager.get()
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
return (stateManager.getGlobalSettingsKey(providerKey) as string) || ""
return (stateManager.getGlobalSettingsKey(providerKey) as string) || "anthropic"
}, [mode, activePanel])
// Get model ID based on current mode and provider
// Different providers use different state keys (e.g., cline uses actModeOpenRouterModelId)
// Re-read when activePanel changes (settings panel closes) to pick up changes
// Falls back to provider's default model if no model has been explicitly set
const modelId = useMemo(() => {
if (!provider) return ""
const stateManager = StateManager.get()
const modelKey = getProviderModelIdKey(provider as ApiProvider, mode)
return (stateManager.getGlobalSettingsKey(modelKey) as string) || getProviderDefaultModelId(provider as ApiProvider) || ""
}, [mode, provider, activePanel])
const toggleMode = useCallback(async () => {
const newMode: Mode = mode === "act" ? "plan" : "act"
setMode(newMode)
@@ -525,29 +444,16 @@ export const ChatView: React.FC<ChatViewProps> = ({
}, [mode, ctrl, textInput, pastedTexts])
// Clear the terminal view and reset task state (used by /clear and "Start New Task" button)
// This is async to ensure clearTask() completes before we remount, preventing race conditions
// where the old messages get fetched and restored before the controller clears them.
const clearViewAndResetTask = useCallback(async () => {
// First, clear the task on the controller and wait for it to finish
// This ensures the controller has no messages before we remount
if (ctrl) {
await ctrl.clearTask()
}
// Now clear the terminal and reset React state
const clearViewAndResetTask = useCallback(() => {
process.stdout.write("\x1b[2J\x1b[3J\x1b[H") // Clear screen + scrollback, cursor home
setTaskSwitchKey((k) => k + 1) // Force remount for fresh Static instance
clearState() // Force clear React state (bypasses empty messages check)
if (ctrl) {
ctrl.clearTask().then(() => ctrl.postStateToWebview())
}
setTextInput("")
setCursorPos(0)
// Clear persisted state
inputStateStorage.delete(storageKey)
// Post the now-empty state
if (ctrl) {
ctrl.postStateToWebview()
}
}, [ctrl, clearState, storageKey])
}, [ctrl, clearState])
const refs = useRef({
searchTimeout: null as NodeJS.Timeout | null,
@@ -619,7 +525,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
section: cmd.section || "default",
cliCompatible: true,
}))
setAvailableCommands([...cliOnlyCommands, ...sortCommandsWorkflowsFirst(cliCommands)])
setAvailableCommands(sortCommandsWorkflowsFirst([...cliCommands, ...cliOnlyCommands]))
} catch {
// Fallback: commands will be empty, menu won't show
}
@@ -654,14 +560,11 @@ export const ChatView: React.FC<ChatViewProps> = ({
if (m.say === "checkpoint_created") return false
if (m.say === "api_req_started") return false
if (m.say === "api_req_retried") return false // Redundant with error_retry messages
if (m.say === "reasoning") return false // Hide thinking traces - they clutter the UI
return true
})
// Combine hook messages with their output, then command messages (like webview does)
// CLI always has hooks enabled, so we always apply combineHookSequences
const withHooks = combineHookSequences(filtered)
return combineCommandSequences(withHooks)
// Combine command messages with their output (like webview does)
return combineCommandSequences(filtered)
}, [messages])
// Detect task switches by watching first message timestamp change.
@@ -808,8 +711,6 @@ export const ChatView: React.FC<ChatViewProps> = ({
setCursorPos(0)
setPastedTexts(new Map()) // Clear stored pastes
pasteCounterRef.current = 0
// Clear persisted state
inputStateStorage.delete(storageKey)
try {
await ctrl.task.handleWebviewAskResponse(responseType, expandedText)
@@ -817,7 +718,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
// Controller may be disposed
}
},
[ctrl, pendingAsk, pastedTexts, storageKey],
[ctrl, pendingAsk, pastedTexts],
)
// Handle cancel/interrupt
@@ -860,13 +761,8 @@ export const ChatView: React.FC<ChatViewProps> = ({
sendAskResponse("yesButtonClicked")
break
case "reject":
// Check for states that should trigger exit (all end-of-task states with Exit button)
if (
pendingAsk?.ask === "resume_task" ||
pendingAsk?.ask === "resume_completed_task" ||
pendingAsk?.ask === "completion_result" ||
pendingAsk?.ask === "new_task"
) {
// Check for resume states that should trigger exit
if (pendingAsk?.ask === "resume_task" || pendingAsk?.ask === "resume_completed_task") {
handleExit()
} else {
sendAskResponse("noButtonClicked")
@@ -885,7 +781,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
await ctrl.initTask(pendingAsk.text || "")
} else {
// From completion_result or resume_completed_task - full clear
await clearViewAndResetTask()
clearViewAndResetTask()
}
break
case "cancel":
@@ -893,7 +789,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
break
}
},
[controller, taskController, sendAskResponse, pendingAsk, handleExit, handleCancel, clearViewAndResetTask],
[controller, taskController, sendAskResponse, pendingAsk, handleExit, handleCancel],
)
// Handle task submission (new task)
@@ -908,8 +804,6 @@ export const ChatView: React.FC<ChatViewProps> = ({
setCursorPos(0)
setPastedTexts(new Map()) // Clear stored pastes
pasteCounterRef.current = 0
// Clear persisted state
inputStateStorage.delete(storageKey)
try {
// Convert image paths to data URLs if needed
@@ -937,12 +831,10 @@ export const ChatView: React.FC<ChatViewProps> = ({
onError?.()
}
},
[ctrl, onError, pastedTexts, storageKey],
[ctrl, onError, pastedTexts],
)
// Auto-submit initial prompt if provided
// When taskId is also provided, this sends the prompt to resume the existing task
// When no taskId, this creates a new task with the prompt
useEffect(() => {
const autoSubmit = async () => {
if (!initialPrompt && (!initialImages || initialImages.length === 0)) {
@@ -963,32 +855,8 @@ export const ChatView: React.FC<ChatViewProps> = ({
if (initialPrompt) {
setTerminalTitle(initialPrompt)
}
if (taskId) {
// Resuming an existing task with a prompt - wait for task to load first
// The task loading happens in the other useEffect via showTaskWithId
// We need to wait for it to complete before sending the resume message
const task = await waitFor(() => ctrl.task, 5000)
if (task) {
// Send the prompt as a message to resume the task
await task.handleWebviewAskResponse("messageResponse", initialPrompt || "")
} else {
// Task failed to load, fall back to creating new task
Logger.error(`Failed to load task ${taskId} for resume, creating new task instead`)
await ctrl.initTask(
initialPrompt || "",
initialImages && initialImages.length > 0 ? initialImages : undefined,
)
}
} else {
// New task - use initTask
// initialImages are already data URLs from index.ts processing
await ctrl.initTask(
initialPrompt || "",
initialImages && initialImages.length > 0 ? initialImages : undefined,
)
}
// initialImages are already data URLs from index.ts processing
await ctrl.initTask(initialPrompt || "", initialImages && initialImages.length > 0 ? initialImages : undefined)
} catch (_error) {
onError?.()
}
@@ -1052,48 +920,19 @@ export const ChatView: React.FC<ChatViewProps> = ({
}, [mentionInfo.inMentionMode, mentionInfo.query, workspacePath])
// Handle keyboard input
//
// KEYBOARD PRIORITY ORDER (first match wins):
// 1. Mouse escape sequences -> filtered out (from AsciiMotionCli tracking)
// 2. Option+arrow escape sequences -> word navigation (handleKeyboardSequence)
// 3. Option+arrow via key.meta -> word navigation (backup for when Ink parses it)
// 4. Panel open -> bail (let panel handle its own input)
// 5. Slash menu open -> menu navigation (up/down/tab/return/escape)
// 6. File menu open -> menu navigation (up/down/tab/return/escape)
// 7. History navigation -> up/down when input empty or matches history item
// 8. Button actions -> "1"/"2" keys when buttons shown and no text typed
// 9. Ask responses -> return to send, numbers for option selection
// 10. Ctrl shortcuts -> Ctrl+A/E/W/U (handleCtrlShortcut)
// 11. Large paste detection -> collapse into placeholder
// 12. Normal input -> tab (mode toggle), return (submit), backspace, arrows, text
//
// Note: Home/End keys are handled separately by useHomeEndKeys hook because
// Ink doesn't expose them in useInput (it sets input='' for these keys).
//
useInput((input, key) => {
// 1. Filter out mouse escape sequences from AsciiMotionCli's mouse tracking
// Handle Ctrl+C - hide input and exit gracefully
if (input === "\x03" || (key.ctrl && input === "c")) {
handleExit()
return
}
// Filter out mouse escape sequences from AsciiMotionCli's mouse tracking
if (isMouseEscapeSequence(input)) {
return
}
// 2. Handle Option+arrow escape sequences for word navigation
if (handleKeyboardSequence(input)) {
return
}
// 3. Handle Option+arrow via key.meta (backup - Ink sometimes parses these instead of passing raw sequence)
if (key.meta) {
if (key.leftArrow) {
setCursorPos(findWordStart(textInputRef.current, cursorPosRef.current))
return
}
if (key.rightArrow) {
setCursorPos(findWordEnd(textInputRef.current, cursorPosRef.current))
return
}
}
// 4. When a panel is open, let the panel handle its own input
// When a panel is open, let the panel handle its own input
if (activePanel) {
return
}
@@ -1101,7 +940,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
const inSlashMenu = slashInfo.inSlashMode && filteredCommands.length > 0 && !slashMenuDismissed
const inFileMenu = mentionInfo.inMentionMode && fileResults.length > 0 && !inSlashMenu
// 5. Slash command menu navigation (takes priority over file menu)
// Slash command menu navigation (takes priority over file menu)
if (inSlashMenu) {
if (key.upArrow) {
setSelectedSlashIndex((i) => Math.max(0, i - 1))
@@ -1115,14 +954,6 @@ export const ChatView: React.FC<ChatViewProps> = ({
const cmd = filteredCommands[selectedSlashIndex]
if (cmd) {
// Handle CLI-only commands locally
if (cmd.name === "help") {
setActivePanel({ type: "help" })
setTextInput("")
setCursorPos(0)
setSelectedSlashIndex(0)
setSlashMenuDismissed(true)
return
}
if (cmd.name === "settings") {
setActivePanel({ type: "settings" })
setTextInput("")
@@ -1132,16 +963,9 @@ export const ChatView: React.FC<ChatViewProps> = ({
return
}
if (cmd.name === "models") {
const apiConfig = StateManager.get().getApiConfiguration()
// Use current mode's provider to determine picker type
const provider =
mode === "act"
? apiConfig.actModeApiProvider || apiConfig.planModeApiProvider
: apiConfig.planModeApiProvider || apiConfig.actModeApiProvider
const initialMode = !provider ? undefined : provider === "cline" ? "featured-models" : "model-picker"
// Set model for current mode (plan or act)
const initialModelKey = mode === "act" ? "actModelId" : "planModelId"
setActivePanel({ type: "settings", initialMode, initialModelKey })
// If separate models for plan/act is enabled, just open settings (user picks which mode)
const hasSeparateModels = StateManager.get().getGlobalSettingsKey("planActSeparateModelsSetting")
setActivePanel({ type: "settings", initialMode: hasSeparateModels ? undefined : "model-picker" })
setTextInput("")
setCursorPos(0)
setSelectedSlashIndex(0)
@@ -1162,10 +986,6 @@ export const ChatView: React.FC<ChatViewProps> = ({
setSlashMenuDismissed(true)
return
}
if (cmd.name === "exit") {
handleExit()
return
}
const newText = insertSlashCommand(textInput, slashInfo.slashIndex, cmd.name)
setTextInput(newText)
setCursorPos(newText.length)
@@ -1181,7 +1001,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
}
}
// 6. File mention menu navigation
// File mention menu navigation
if (inFileMenu) {
if (key.upArrow) {
setSelectedIndex((i) => Math.max(0, i - 1))
@@ -1209,7 +1029,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
}
}
// 7. History navigation with up/down arrows
// History navigation with up/down arrows
// Only works when: input is empty, or input matches the currently selected history item
if (key.upArrow && !inSlashMenu && !inFileMenu) {
const historyItems = getHistoryItems()
@@ -1259,7 +1079,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
}
}
// 8. Handle button actions (1 for primary, 2 for secondary)
// Handle button actions (1 for primary, 2 for secondary)
// Only when buttons are enabled, not streaming, and no text has been typed
if (
buttonConfig.enableButtons &&
@@ -1274,8 +1094,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
if (hasPrimary && buttonConfig.primaryAction) {
handleButtonAction(buttonConfig.primaryAction, true)
return
}
if (hasSecondary && !hasPrimary && buttonConfig.secondaryAction) {
} else if (hasSecondary && !hasPrimary && buttonConfig.secondaryAction) {
handleButtonAction(buttonConfig.secondaryAction, false)
return
}
@@ -1287,7 +1106,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
}
}
// 9. Handle ask responses for options and text input
// Handle ask responses for options and text input
if (pendingAsk && !isYoloSuppressed(yolo, pendingAsk.ask as ClineAsk | undefined)) {
// Allow sending text message for any ask type where sending is enabled
if (key.return && textInput.trim() && !buttonConfig.sendingDisabled) {
@@ -1296,7 +1115,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
}
// Number selection for options (only when no text typed yet)
if (askType === "options") {
const num = Number.parseInt(input, 10)
const num = parseInt(input, 10)
if (textInput === "" && !Number.isNaN(num) && num >= 1 && num <= askOptions.length) {
const selectedOption = askOptions[num - 1]
sendAskResponse("messageResponse", selectedOption)
@@ -1305,12 +1124,40 @@ export const ChatView: React.FC<ChatViewProps> = ({
}
}
// 10. Handle Ctrl+ shortcuts (Ctrl+A, Ctrl+E, Ctrl+W, etc.)
if (key.ctrl && input && handleCtrlShortcut(input)) {
return
// Handle Ctrl+ shortcuts
const keydown = input?.toLowerCase()
if (key.ctrl && cursorPos && keydown) {
switch (keydown) {
case "u": // Ctrl+U, clear line before cursor
if (cursorPos > 0) {
setTextInput((prev) => prev.slice(cursorPos))
setCursorPos(0)
}
return
case "e": // Ctrl+E, move cursor to end
setCursorPos(textInput.length)
return
case "b": // Ctrl+B, move cursor left
setCursorPos((pos) => Math.max(0, pos - 1))
return
case "f": // Ctrl+F, move cursor right
setCursorPos((pos) => Math.min(textInput.length, pos + 1))
return
case "d": // Ctrl+D, delete character after cursor
if (cursorPos < textInput.length) {
setTextInput((prev) => prev.slice(0, cursorPos) + prev.slice(cursorPos + 1))
}
return
case "h": // Ctrl+H, delete character before cursor (like backspace)
if (cursorPos > 0) {
setTextInput((prev) => prev.slice(0, cursorPos - 1) + prev.slice(cursorPos))
setCursorPos((pos) => pos - 1)
}
return
}
}
// 11. Detect paste by checking if input length exceeds threshold
// Detect paste by checking if input length exceeds threshold
// Large pastes mess up the terminal UI, so we collapse them into a placeholder
// Terminal sends large pastes in multiple chunks, so we combine chunks that arrive rapidly
if (input && input.length > PASTE_COLLAPSE_THRESHOLD) {
@@ -1338,10 +1185,10 @@ export const ChatView: React.FC<ChatViewProps> = ({
}
pasteUpdateTimeoutRef.current = setTimeout(() => {
const newPlaceholder = `[Pasted text #${pasteNum} +${activePasteLinesRef.current} lines]`
const pattern = new RegExp(`\\[Pasted text #${pasteNum} \\+\\d+ lines\\]`)
const newText = textInputRef.current.replace(pattern, newPlaceholder)
textInputRef.current = newText // Update ref immediately so setCursorPos bounds check works
setTextInput(newText)
setTextInput((prev) => {
const pattern = new RegExp(`\\[Pasted text #${pasteNum} \\+\\d+ lines\\]`)
return prev.replace(pattern, newPlaceholder)
})
// Update cursor to be right after the placeholder
setCursorPos(activePasteStartPosRef.current + newPlaceholder.length)
Logger.info(`Paste #${pasteNum} complete: ${activePasteLinesRef.current} lines`)
@@ -1354,8 +1201,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
pasteCounterRef.current += 1
const pasteNum = pasteCounterRef.current
activePasteNumRef.current = pasteNum
const currentCursorPos = cursorPosRef.current // Use ref to avoid stale closure
activePasteStartPosRef.current = currentCursorPos // Track where placeholder starts
activePasteStartPosRef.current = cursorPos // Track where placeholder starts
// Count line breaks in the pasted content (handle both \n and \r)
const extraLines = input.match(/[\r\n]/g)?.length || 0
activePasteLinesRef.current = extraLines // Track total lines
@@ -1367,31 +1213,31 @@ export const ChatView: React.FC<ChatViewProps> = ({
return next
})
const newText =
textInputRef.current.slice(0, currentCursorPos) + placeholder + textInputRef.current.slice(currentCursorPos)
textInputRef.current = newText // Update ref immediately so setCursorPos bounds check works
setTextInput(newText)
setCursorPos(currentCursorPos + placeholder.length)
setTextInput((prev) => prev.slice(0, cursorPos) + placeholder + prev.slice(cursorPos))
setCursorPos(cursorPos + placeholder.length)
return // Exit early - don't also add the raw input via normal handling below
}
// 12. Normal input handling
// Normal input handling
if (key.shift && key.tab) {
toggleAutoApproveAll()
toggleYolo()
return
}
if (key.tab && !mentionInfo.inMentionMode && !slashInfo.inSlashMode) {
toggleMode()
return
}
if (key.return && !mentionInfo.inMentionMode && !slashInfo.inSlashMode && !pendingAsk && !isSpinnerActive) {
if (key.return && !mentionInfo.inMentionMode && !slashInfo.inSlashMode && !pendingAsk) {
if (prompt.trim() || imagePaths.length > 0) {
handleSubmit(prompt.trim(), imagePaths)
}
return
}
if (key.backspace || key.delete) {
deleteCharBefore()
if (cursorPos > 0) {
setTextInput((prev) => prev.slice(0, cursorPos - 1) + prev.slice(cursorPos))
setCursorPos((pos) => pos - 1)
}
return
}
// Cursor movement (when not in a menu)
@@ -1400,20 +1246,21 @@ export const ChatView: React.FC<ChatViewProps> = ({
return
}
if (key.rightArrow && !inSlashMenu && !inFileMenu) {
setCursorPos((pos) => Math.min(textInputRef.current.length, pos + 1))
setCursorPos((pos) => Math.min(textInput.length, pos + 1))
return
}
if (key.upArrow && !inSlashMenu && !inFileMenu) {
setCursorPos(moveCursorUp(textInputRef.current, cursorPosRef.current))
setCursorPos(moveCursorUp(textInput, cursorPos))
return
}
if (key.downArrow && !inSlashMenu && !inFileMenu) {
setCursorPos(moveCursorDown(textInputRef.current, cursorPosRef.current))
setCursorPos(moveCursorDown(textInput, cursorPos))
return
}
// Normal input (single char or short paste)
if (input && !key.ctrl && !key.meta && !key.upArrow && !key.downArrow && !key.tab) {
insertTextAtCursor(input)
setTextInput((prev) => prev.slice(0, cursorPos) + input + prev.slice(cursorPos))
setCursorPos((pos) => pos + input.length)
}
})
@@ -1474,10 +1321,10 @@ export const ChatView: React.FC<ChatViewProps> = ({
{/* Dynamic region - only current streaming message + input */}
<Box flexDirection="column" width="100%">
{/* Animated robot and welcome text - only shown before messages start and user hasn't interacted */}
{/* Animated robot and welcome text - only shown before messages start and user hasn't scrolled */}
{isWelcomeState && (
<Box flexDirection="column" marginBottom={1}>
<AsciiMotionCli onInteraction={() => setUserScrolled(true)} />
<AsciiMotionCli onScroll={() => setUserScrolled(true)} robotTopRow={robotTopRow} />
<Text> </Text>
<Text bold color="white">
{centerText("What can I do for you?")}
@@ -1528,7 +1375,6 @@ export const ChatView: React.FC<ChatViewProps> = ({
<SettingsPanelContent
controller={ctrl}
initialMode={activePanel.initialMode}
initialModelKey={activePanel.initialModelKey}
onClose={() => setActivePanel(null)}
/>
)}
@@ -1542,9 +1388,6 @@ export const ChatView: React.FC<ChatViewProps> = ({
/>
)}
{/* Help panel */}
{activePanel?.type === "help" && <HelpPanelContent onClose={() => setActivePanel(null)} />}
{/* Slash command menu - below input (takes priority over file menu) */}
{showSlashMenu && !activePanel && (
<Box paddingLeft={1} paddingRight={1}>
@@ -1584,7 +1427,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
{/* Row 1: Instructions (left, can wrap) | Plan/Act toggle (right, no wrap) */}
<Box justifyContent="space-between" paddingLeft={1} paddingRight={1} width="100%">
<Box flexShrink={1} flexWrap="wrap">
<Text color="gray">/ for commands · @ for files</Text>
<Text color="gray">@ for files · / for commands</Text>
</Box>
<Box flexShrink={0} gap={1}>
<Box>
@@ -1636,17 +1479,17 @@ export const ChatView: React.FC<ChatViewProps> = ({
{/* Row 4: Auto-approve toggle */}
<Box paddingLeft={1} paddingRight={1}>
{autoApproveAll ? (
<Text>
<Text color="green"> Auto-approve all enabled</Text>
<Text color="gray"> (Shift+Tab)</Text>
</Text>
{yolo ? (
<Text color="green"> Auto-approve all enabled (Shift+Tab)</Text>
) : (
<Text color="gray">Auto-approve all disabled (Shift+Tab)</Text>
)}
</Box>
</Box>
)}
{/* Session summary - shown when exiting */}
{isExiting && <SessionSummary />}
</Box>
</Box>
)
@@ -13,7 +13,6 @@ import {
import { Box, Text, useApp, useInput } from "ink"
import React, { useMemo, useState } from "react"
import { useStdinContext } from "../context/StdinContext"
import { fuzzyFilter } from "../utils/fuzzy-search"
import {
BooleanSelect,
buildConfigEntries,
@@ -22,8 +21,6 @@ import {
HookInfo,
HookRow,
MAX_VISIBLE,
ObjectEditorPanel,
ObjectEditorState,
parseValue,
SEPARATOR,
SectionHeader,
@@ -108,8 +105,6 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
const [isEditing, setIsEditing] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(0)
const [editValue, setEditValue] = useState("")
const [searchQuery, setSearchQuery] = useState("")
const [objectEditor, setObjectEditor] = useState<ObjectEditorState | null>(null)
// Build entries for settings tab
const configEntries = useMemo(
@@ -117,13 +112,6 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
[globalState, workspaceState],
)
const filteredConfigEntries = useMemo(() => {
if (!searchQuery.trim()) {
return configEntries
}
return fuzzyFilter(configEntries, searchQuery, (entry) => `${entry.key} ${String(entry.value ?? "")}`)
}, [configEntries, searchQuery])
// Build entries for rules tab
const ruleEntries = useMemo(() => {
const entries: ToggleEntry[] = []
@@ -171,7 +159,7 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
const currentListLength = useMemo(() => {
switch (currentTab) {
case "settings":
return filteredConfigEntries.length
return configEntries.length
case "rules":
return ruleEntries.length
case "workflows":
@@ -183,14 +171,7 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
default:
return 0
}
}, [
currentTab,
filteredConfigEntries.length,
ruleEntries.length,
workflowEntries.length,
hookEntries.length,
skillEntries.length,
])
}, [currentTab, configEntries.length, ruleEntries.length, workflowEntries.length, hookEntries.length, skillEntries.length])
// Get available tabs
const availableTabs = useMemo(() => {
@@ -210,11 +191,10 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
setCurrentTab(newTab)
setSelectedIndex(0)
setIsEditing(false)
setObjectEditor(null)
}
// Settings tab handlers
const selectedConfigEntry = filteredConfigEntries[selectedIndex]
const selectedConfigEntry = configEntries[selectedIndex]
const handleSettingsSave = (value: string | boolean) => {
if (!selectedConfigEntry) {
@@ -230,43 +210,6 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
setIsEditing(false)
}
const getObjectAtPath = (root: Record<string, unknown>, path: string[]): Record<string, unknown> => {
let current: unknown = root
for (const segment of path) {
if (!current || typeof current !== "object") {
return {}
}
current = (current as Record<string, unknown>)[segment]
}
return current && typeof current === "object" ? (current as Record<string, unknown>) : {}
}
const setObjectValueAtPath = (
root: Record<string, unknown>,
path: string[],
key: string,
value: unknown,
): Record<string, unknown> => {
if (path.length === 0) {
return { ...root, [key]: value }
}
const [head, ...rest] = path
const child = root[head]
const childObj = child && typeof child === "object" ? (child as Record<string, unknown>) : {}
return {
...root,
[head]: setObjectValueAtPath(childObj, rest, key, value),
}
}
const persistObjectEditor = (nextObject: Record<string, unknown>, source: "global" | "workspace", key: string) => {
if (source === "global" && onUpdateGlobal) {
onUpdateGlobal(key as GlobalStateAndSettingsKey, nextObject as never)
} else if (source === "workspace" && onUpdateWorkspace) {
onUpdateWorkspace(key as LocalStateKey, nextObject as never)
}
}
const handleSettingsReset = () => {
if (!selectedConfigEntry?.isEditable || selectedConfigEntry.source !== "global") {
return
@@ -297,22 +240,15 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
// Input handling
useInput(
(input, key) => {
if (objectEditor) {
return
}
if (key.escape) {
if (input.toLowerCase() === "q" || key.escape) {
exit()
}
if (key.leftArrow || key.rightArrow || (input >= "1" && input <= "5")) {
const currentTabIndex = availableTabs.findIndex((t) => t.key === currentTab)
const targetIdx =
input >= "1" && input <= "5"
? Number.parseInt(input) - 1
: key.leftArrow
? (currentTabIndex - 1 + availableTabs.length) % availableTabs.length
: (currentTabIndex + 1) % availableTabs.length
// Tab navigation with Tab key or number keys
if (key.tab || (input >= "1" && input <= "5")) {
const targetIdx = key.tab
? (availableTabs.findIndex((t) => t.key === currentTab) + 1) % availableTabs.length
: parseInt(input) - 1
if (targetIdx >= 0 && targetIdx < availableTabs.length) {
handleTabChange(availableTabs[targetIdx].key)
}
@@ -320,45 +256,21 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
}
// List navigation (arrow keys and vim-style j/k)
if (key.upArrow) {
if (key.upArrow || input === "k") {
setSelectedIndex((i) => (i > 0 ? i - 1 : currentListLength - 1))
} else if (key.downArrow) {
} else if (key.downArrow || input === "j") {
setSelectedIndex((i) => (i < currentListLength - 1 ? i + 1 : 0))
}
// Tab-specific actions
if (currentTab === "settings") {
if ((key.return || key.tab) && selectedConfigEntry?.isEditable) {
if (selectedConfigEntry.type === "boolean") {
handleSettingsSave(!selectedConfigEntry.value)
return
}
if (selectedConfigEntry.type === "object") {
const value =
selectedConfigEntry.value && typeof selectedConfigEntry.value === "object"
? (selectedConfigEntry.value as Record<string, unknown>)
: {}
setObjectEditor({
source: selectedConfigEntry.source,
key: selectedConfigEntry.key,
path: [],
value,
selectedIndex: 0,
isEditingValue: false,
editValue: "",
})
return
}
if ((key.return || input === "e") && selectedConfigEntry?.isEditable) {
setEditValue(selectedConfigEntry.value !== undefined ? String(selectedConfigEntry.value) : "")
setIsEditing(true)
} else if (key.ctrl && input.toLowerCase() === "r") {
} else if (input === "r") {
handleSettingsReset()
} else if (key.backspace || key.delete) {
setSearchQuery((prev) => prev.slice(0, -1))
} else if (input && !key.ctrl && !key.meta && !key.escape && !key.upArrow && !key.downArrow) {
setSearchQuery((prev) => prev + input)
}
} else if (key.return || key.tab || input === " ") {
} else if (key.return || input === " ") {
// Toggle for rules/workflows/hooks/skills
handleToggle()
}
@@ -426,31 +338,13 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
)
}
if (objectEditor && currentTab === "settings") {
return (
<ObjectEditorPanel
getObjectAtPath={getObjectAtPath}
onClose={() => setObjectEditor(null)}
onPersist={(nextObject) => persistObjectEditor(nextObject, objectEditor.source, objectEditor.key)}
setObjectValueAtPath={setObjectValueAtPath}
setState={setObjectEditor}
state={objectEditor}
/>
)
}
// Render tab content
const renderTabContent = () => {
switch (currentTab) {
case "settings": {
const visibleEntries = filteredConfigEntries.slice(startIndex, startIndex + MAX_VISIBLE)
const visibleEntries = configEntries.slice(startIndex, startIndex + MAX_VISIBLE)
return (
<React.Fragment>
<Box>
<Text>Search: </Text>
<Text color="white">{searchQuery}</Text>
<Text inverse> </Text>
</Box>
<Box>
<Text>Data directory: </Text>
<Text color="blue" underline>
@@ -613,12 +507,12 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
// Help text based on current tab
const getHelpText = () => {
const base = "↑/↓ Navigate • ←/→ tabs • 1-5 tabs • Esc Exit"
const base = "↑/↓/j/k Navigate • Tab/1-5 Switch tabs • q/Esc Exit"
if (currentTab === "settings") {
return `${base} Type to search • Enter/Tab Edit (booleans toggle) • Backspace clear search • Ctrl+R Reset`
return `${base} • Enter/e Edit • r Reset`
}
const openFolder = onOpenFolder ? " • o Open folder" : ""
return `${base} • Enter/Tab/Space Toggle${openFolder}`
return `${base} • Enter/Space Toggle${openFolder}`
}
return (
@@ -46,19 +46,16 @@ export interface SkillInfo {
enabled: boolean
}
export interface ObjectEditorState {
source: "global" | "workspace"
key: string
path: string[]
value: Record<string, unknown>
selectedIndex: number
isEditingValue: boolean
editValue: string
}
export const EXCLUDED_KEYS = new Set([
"taskHistory",
"primaryRootIndex",
"subagentsEnabled",
"subagentTerminalOutputLineLimit",
"welcomeViewCompleted",
"isNewUser",
])
export const EXCLUDED_KEYS = new Set(["taskHistory", "primaryRootIndex", "welcomeViewCompleted", "isNewUser"])
export const EDITABLE_TYPES: Set<ValueType> = new Set(["string", "number", "boolean", "object"])
export const EDITABLE_TYPES: Set<ValueType> = new Set(["string", "number", "boolean"])
export const MAX_VISIBLE = 12
export const SEPARATOR = "─".repeat(80)
@@ -138,7 +135,7 @@ export function parseValue(input: string, type: ValueType): unknown {
return input.toLowerCase() === "true" || input === "1"
}
if (type === "number") {
const num = Number.parseFloat(input)
const num = parseFloat(input)
return Number.isNaN(num) ? 0 : num
}
if (type === "object") {
@@ -220,7 +217,7 @@ export const TextInput: React.FC<TextInputProps> = ({ label, onChange, onCancel,
</Text>
<Box>
<Text color="white">{value}</Text>
<Text color="cyan">|</Text>
<Text inverse> </Text>
</Box>
<Text color="gray">Type: {type} Enter to save Esc to cancel</Text>
</Box>
@@ -382,169 +379,3 @@ export const SectionHeader: React.FC<{ title: string }> = ({ title }) => (
</Text>
</Box>
)
interface ObjectEditorPanelProps {
state: ObjectEditorState
setState: React.Dispatch<React.SetStateAction<ObjectEditorState | null>>
onClose: () => void
onPersist: (nextObject: Record<string, unknown>) => void
getObjectAtPath: (root: Record<string, unknown>, path: string[]) => Record<string, unknown>
setObjectValueAtPath: (root: Record<string, unknown>, path: string[], key: string, value: unknown) => Record<string, unknown>
}
export const ObjectEditorPanel: React.FC<ObjectEditorPanelProps> = ({
state,
setState,
onClose,
onPersist,
getObjectAtPath,
setObjectValueAtPath,
}) => {
const { isRawModeSupported } = useStdinContext()
const currentNode = getObjectAtPath(state.value, state.path)
const objectEntries = Object.entries(currentNode).sort(([a], [b]) => a.localeCompare(b))
const selectedEntry = objectEntries[state.selectedIndex]
const breadcrumb = [state.key, ...state.path].join(" ")
useInput(
(input, key) => {
if (state.isEditingValue) {
if (key.escape) {
setState((prev) => (prev ? { ...prev, isEditingValue: false, editValue: "" } : prev))
return
}
if (key.return) {
if (!selectedEntry) {
setState((prev) => (prev ? { ...prev, isEditingValue: false, editValue: "" } : prev))
return
}
const [entryKey, entryValue] = selectedEntry
let parsed: unknown = state.editValue
if (typeof entryValue === "boolean") {
parsed = state.editValue.toLowerCase() === "true" || state.editValue === "1"
} else if (typeof entryValue === "number") {
const maybeNum = Number(state.editValue)
parsed = Number.isNaN(maybeNum) ? 0 : maybeNum
}
const nextObject = setObjectValueAtPath(state.value, state.path, entryKey, parsed)
onPersist(nextObject)
setState((prev) => (prev ? { ...prev, value: nextObject, isEditingValue: false, editValue: "" } : prev))
return
}
if (key.backspace || key.delete) {
setState((prev) => (prev ? { ...prev, editValue: prev.editValue.slice(0, -1) } : prev))
return
}
if (input && !key.ctrl && !key.meta) {
setState((prev) => (prev ? { ...prev, editValue: prev.editValue + input } : prev))
}
return
}
if (key.escape) {
if (state.path.length > 0) {
setState((prev) => (prev ? { ...prev, path: prev.path.slice(0, -1), selectedIndex: 0 } : prev))
} else {
onClose()
}
return
}
if (key.upArrow || input === "k") {
setState((prev) =>
prev
? {
...prev,
selectedIndex:
objectEntries.length > 0
? prev.selectedIndex > 0
? prev.selectedIndex - 1
: objectEntries.length - 1
: 0,
}
: prev,
)
return
}
if (key.downArrow || input === "j") {
setState((prev) =>
prev
? {
...prev,
selectedIndex:
objectEntries.length > 0
? prev.selectedIndex < objectEntries.length - 1
? prev.selectedIndex + 1
: 0
: 0,
}
: prev,
)
return
}
if (key.return || key.tab) {
if (!selectedEntry) {
return
}
const [entryKey, entryValue] = selectedEntry
if (typeof entryValue === "boolean") {
const nextObject = setObjectValueAtPath(state.value, state.path, entryKey, !entryValue)
onPersist(nextObject)
setState((prev) => (prev ? { ...prev, value: nextObject } : prev))
return
}
if (entryValue && typeof entryValue === "object" && !Array.isArray(entryValue)) {
setState((prev) => (prev ? { ...prev, path: [...prev.path, entryKey], selectedIndex: 0 } : prev))
return
}
setState((prev) =>
prev
? { ...prev, isEditingValue: true, editValue: entryValue !== undefined ? String(entryValue) : "" }
: prev,
)
}
},
{ isActive: isRawModeSupported },
)
return (
<Box flexDirection="column">
<Text bold color="white">
Edit Nested Object
</Text>
<Text color="gray">{SEPARATOR}</Text>
<Text color="cyan">{breadcrumb}</Text>
{state.isEditingValue ? (
<Box flexDirection="column" marginTop={1}>
<Box>
<Text color="white">{state.editValue}</Text>
<Text color="cyan">|</Text>
</Box>
<Text color="gray">Enter to save Esc to cancel</Text>
</Box>
) : (
<Box flexDirection="column" marginTop={1}>
{objectEntries.length === 0 ? (
<Text color="gray">No nested keys at this level.</Text>
) : (
objectEntries.map(([key, value], idx) => {
const isSelected = idx === state.selectedIndex
const valueText =
value && typeof value === "object" && !Array.isArray(value) ? "{...}" : String(value)
return (
<Text color={isSelected ? "cyan" : undefined} key={key}>
{isSelected ? " " : " "}
<Text color="cyan">{key}</Text>
<Text color="gray">: </Text>
<Text color="white">{valueText}</Text>
</Text>
)
})
)}
<Text color="gray">/ Navigate Enter/Tab Edit or drill in Esc Back/Close</Text>
</Box>
)}
</Box>
)
}
+216
View File
@@ -0,0 +1,216 @@
/**
* DiffView component for displaying file diffs in Ink
* Shows unified diff output with colored lines for additions/deletions
* Supports SEARCH/REPLACE format and ApplyPatch format
*/
import { Box, Text } from "ink"
import React from "react"
interface DiffViewProps {
/** Diff content (SEARCH/REPLACE format, ApplyPatch format, or raw content for new files) */
content?: string
}
interface DiffLine {
type: "add" | "remove" | "context" | "separator"
content: string
}
interface ParsedPatch {
additions: number
deletions: number
lines: DiffLine[]
}
// Constants for format markers
const MARKERS = {
SEARCH_BLOCK: "------- SEARCH",
SEARCH_SEPARATOR: "=======",
REPLACE_BLOCK: "+++++++ REPLACE",
NEW_BEGIN: "*** Begin Patch",
NEW_END: "*** End Patch",
} as const
/**
* Parse SEARCH/REPLACE format into diff lines
* Format: ------- SEARCH\n...\n=======\n...\n+++++++ REPLACE
*/
function parseSearchReplaceFormat(content: string): ParsedPatch {
const result: ParsedPatch = { additions: 0, deletions: 0, lines: [] }
// Find all SEARCH blocks
const searchRegex = /-{7,} SEARCH/g
const searchPositions: number[] = []
let match: RegExpExecArray | null
while ((match = searchRegex.exec(content)) !== null) {
searchPositions.push(match.index)
}
for (let i = 0; i < searchPositions.length; i++) {
// Add separator between blocks
if (i > 0) {
result.lines.push({ type: "separator", content: "" })
}
const start = searchPositions[i]
const end = i < searchPositions.length - 1 ? searchPositions[i + 1] : content.length
const blockContent = content.substring(start, end)
// Extract content after SEARCH marker
const afterSearch = blockContent.substring(MARKERS.SEARCH_BLOCK.length).replace(/^\r?\n/, "")
const separatorIndex = afterSearch.indexOf(MARKERS.SEARCH_SEPARATOR)
if (separatorIndex === -1) {
// Still streaming - only SEARCH block available
const searchContent = afterSearch.trimEnd()
for (const line of searchContent.split("\n")) {
result.lines.push({ type: "remove", content: line })
result.deletions++
}
} else {
// Extract SEARCH block (deletions)
const searchContent = afterSearch.substring(0, separatorIndex).replace(/\r?\n$/, "")
for (const line of searchContent.split("\n")) {
result.lines.push({ type: "remove", content: line })
result.deletions++
}
// Extract REPLACE block (additions)
const afterSeparator = afterSearch.substring(separatorIndex + MARKERS.SEARCH_SEPARATOR.length).replace(/^\r?\n/, "")
const replaceEndIndex = afterSeparator.indexOf(MARKERS.REPLACE_BLOCK)
const replaceContent =
replaceEndIndex !== -1
? afterSeparator.substring(0, replaceEndIndex).replace(/\r?\n$/, "")
: afterSeparator.trimEnd()
for (const line of replaceContent.split("\n")) {
result.lines.push({ type: "add", content: line })
result.additions++
}
}
}
return result
}
/**
* Parse ApplyPatch format into diff lines
* Format: *** Begin Patch\n*** Update File: path\n+line\n-line\n*** End Patch
*/
function parseApplyPatchFormat(content: string): ParsedPatch {
const result: ParsedPatch = { additions: 0, deletions: 0, lines: [] }
const beginIndex = content.indexOf(MARKERS.NEW_BEGIN)
if (beginIndex === -1) return result
const endIndex = content.indexOf(MARKERS.NEW_END)
const contentStart = beginIndex + MARKERS.NEW_BEGIN.length
const contentEnd = endIndex !== -1 ? endIndex : content.length
const patchContent = content.substring(contentStart, contentEnd).trim()
for (const line of patchContent.split("\n")) {
// Skip file header lines
if (line.match(/^\*\*\* (Add|Update|Delete) File:/)) continue
if (line.trim() === "@@") continue
if (line.startsWith("+")) {
const hasSpace = line.startsWith("+ ")
result.lines.push({ type: "add", content: hasSpace ? line.slice(2) : line.slice(1) })
result.additions++
} else if (line.startsWith("-")) {
const hasSpace = line.startsWith("- ")
result.lines.push({ type: "remove", content: hasSpace ? line.slice(2) : line.slice(1) })
result.deletions++
} else if (line.trim()) {
// Context line
result.lines.push({ type: "context", content: line })
}
}
return result
}
/**
* Parse tool content into diff lines
* Detects format and delegates to appropriate parser
*/
function parseToolContent(content: string): ParsedPatch {
// Try SEARCH/REPLACE format first
if (content.includes(MARKERS.SEARCH_BLOCK)) {
return parseSearchReplaceFormat(content)
}
// Try ApplyPatch format
if (content.includes(MARKERS.NEW_BEGIN)) {
return parseApplyPatchFormat(content)
}
// Fallback: treat as new file (all additions)
const lines = content.split("\n")
return {
additions: lines.length,
deletions: 0,
lines: lines.map((line) => ({ type: "add", content: line })),
}
}
// Dim diff colors similar to IDE diff views
const DIFF_COLORS = {
addBg: "rgb(35, 61, 41)", // dark muted green
addFg: "rgb(156, 204, 122)", // light green text
removeBg: "rgb(62, 36, 36)", // dark muted red
removeFg: "rgb(224, 139, 139)", // light red/pink text
} as const
/**
* Render a diff line with full-width background color highlighting
* Uses Box with backgroundColor to handle wrapping properly
*/
const DiffLineRow: React.FC<{ line: DiffLine }> = ({ line }) => {
if (line.type === "separator") {
return <Text> </Text>
}
const prefix = line.type === "add" ? "+" : line.type === "remove" ? "-" : " "
const content = prefix + line.content
switch (line.type) {
case "add":
return (
<Box backgroundColor={DIFF_COLORS.addBg} width="100%">
<Text color={DIFF_COLORS.addFg}>{content}</Text>
</Box>
)
case "remove":
return (
<Box backgroundColor={DIFF_COLORS.removeBg} width="100%">
<Text color={DIFF_COLORS.removeFg}>{content}</Text>
</Box>
)
case "context":
return <Text dimColor>{content}</Text>
default:
return null
}
}
/**
* DiffView component that renders file edits as a diff
* Supports SEARCH/REPLACE format and ApplyPatch format
*/
export const DiffView: React.FC<DiffViewProps> = ({ content }) => {
if (!content) {
return null
}
const parsed = parseToolContent(content)
return (
<Box flexDirection="column" width="100%">
{parsed.lines.map((line, idx) => (
<DiffLineRow key={idx} line={line} />
))}
</Box>
)
}
@@ -95,8 +95,8 @@ function parseInput(text: string, availableCommands?: string[]): Segment[] {
})
}
// Always ensure at least one segment exists for stable cursor rendering
if (segments.length === 0) {
// If no segments (empty or whitespace only), add a single normal segment
if (segments.length === 0 && text.length > 0) {
segments.push({
text: text,
type: "normal",
@@ -16,7 +16,6 @@ import {
importFromCodex,
importFromOpenCode,
} from "../utils/import-configs"
import { applyProviderConfig } from "../utils/provider-config"
type ImportStep = "select" | "confirm" | "saving" | "error"
@@ -60,13 +59,23 @@ export const ImportView: React.FC<ImportViewProps> = ({ source, onComplete, onCa
return
}
await applyProviderConfig({
providerId: selectedKey.provider,
apiKey: selectedKey.key,
modelId: selectedKey.modelId,
})
const stateManager = StateManager.get()
stateManager.setGlobalState("welcomeViewCompleted", true)
const config: Record<string, string> = {
actModeApiProvider: selectedKey.provider,
planModeApiProvider: selectedKey.provider,
apiProvider: selectedKey.provider,
}
// Set API key
config[selectedKey.keyField] = selectedKey.key
// Set model ID if available
if (selectedKey.modelId) {
config.actModeApiModelId = selectedKey.modelId
config.planModeApiModelId = selectedKey.modelId
}
stateManager.setApiConfiguration(config)
await stateManager.flushPendingState()
onComplete()
@@ -6,7 +6,6 @@
import { Box, Text } from "ink"
import Spinner from "ink-spinner"
import React, { useEffect, useMemo, useState } from "react"
import { refreshOcaModels } from "@/core/controller/models/refreshOcaModels"
import { refreshOpenRouterModels } from "@/core/controller/models/refreshOpenRouterModels"
import {
type ApiProvider,
@@ -65,7 +64,6 @@ import {
xaiDefaultModelId,
xaiModels,
} from "@/shared/api"
import { StringRequest } from "@/shared/proto/cline/common"
import { filterOpenRouterModelIds } from "@/shared/utils/model-filters"
import { COLORS } from "../constants/colors"
import { getOpenRouterDefaultModelId, usesOpenRouterModels } from "../utils/openrouter-models"
@@ -107,7 +105,7 @@ export function hasStaticModels(provider: string): boolean {
}
export function hasModelPicker(provider: string): boolean {
return hasStaticModels(provider) || usesOpenRouterModels(provider) || provider === "oca"
return hasStaticModels(provider) || usesOpenRouterModels(provider)
}
export function getDefaultModelId(provider: string): string {
@@ -134,7 +132,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({ provider, controller,
const [isLoading, setIsLoading] = useState(false)
const [asyncModels, setAsyncModels] = useState<string[]>([])
// Fetch async models (OpenRouter or OCA) when needed
// Fetch OpenRouter models when needed using shared core function
useEffect(() => {
if (usesOpenRouterModels(provider)) {
setIsLoading(true)
@@ -147,23 +145,11 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({ provider, controller,
.finally(() => {
setIsLoading(false)
})
} else if (provider === "oca") {
setIsLoading(true)
refreshOcaModels(controller, StringRequest.create({ value: "" }))
.then((result) => {
if (result.models) {
const modelIds = Object.keys(result.models).sort((a, b) => a.localeCompare(b))
setAsyncModels(modelIds)
}
})
.finally(() => {
setIsLoading(false)
})
}
}, [provider, controller])
const modelList = useMemo(() => {
if (usesOpenRouterModels(provider) || provider === "oca") {
if (usesOpenRouterModels(provider)) {
return asyncModels
}
return getModelList(provider)
@@ -194,7 +180,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({ provider, controller,
}
// If async fetch returned no models, render nothing
if ((usesOpenRouterModels(provider) || provider === "oca") && modelList.length === 0) {
if (usesOpenRouterModels(provider) && modelList.length === 0) {
return null
}
@@ -20,13 +20,11 @@ interface PanelProps {
tabs?: PanelTab[]
/** Current tab key - required when tabs are provided */
currentTab?: string
/** Whether currently in a subpage (shows "Esc to go back" and hides arrow key hint) */
isSubpage?: boolean
/** Panel content */
children: ReactNode
}
export const Panel: React.FC<PanelProps> = ({ label, tabs, currentTab, isSubpage, children }) => {
export const Panel: React.FC<PanelProps> = ({ label, tabs, currentTab, children }) => {
const { columns } = useTerminalSize()
const currentTabIndex = currentTab && tabs ? tabs.findIndex((t) => t.key === currentTab) : 0
@@ -37,7 +35,7 @@ export const Panel: React.FC<PanelProps> = ({ label, tabs, currentTab, isSubpage
<Text bold color={COLORS.primaryBlue}>
{label}
</Text>
<Text color="gray"> (Esc to {isSubpage ? "go back" : "close"})</Text>
<Text color="gray"> (Esc to close)</Text>
</Box>
{/* Tab bar if tabs are provided */}
@@ -55,7 +53,7 @@ export const Panel: React.FC<PanelProps> = ({ label, tabs, currentTab, isSubpage
</Text>
)
})}
{!isSubpage && <Text color="gray"> (/)</Text>}
<Text color="gray"> (/)</Text>
</Box>
)}
@@ -4,12 +4,25 @@
import React, { useMemo } from "react"
import { StateManager } from "@/core/storage/StateManager"
import type { ApiConfiguration } from "@/shared/api"
import { getProviderLabel, useValidProviders } from "../utils/providers"
import { SearchableList, type SearchableListItem } from "./SearchableList"
import { API_PROVIDERS_LIST, ApiConfiguration } from "@/shared/api"
import providersData from "@/shared/providers/providers.json"
import { SearchableList, SearchableListItem } from "./SearchableList"
// Re-export for backwards compatibility
export { getProviderLabel }
// Create a lookup map from provider value to display label
const providerLabels: Record<string, string> = Object.fromEntries(
providersData.list.map((p: { value: string; label: string }) => [p.value, p.label]),
)
// Get provider order from providers.json (same order as webview)
const providerOrder: string[] = providersData.list.map((p: { value: string }) => p.value)
export function getProviderLabel(providerId: string): string {
return providerLabels[providerId] || providerId
}
export function getProviderOrder(): string[] {
return providerOrder
}
/**
* Check if a provider is configured (has required credentials/settings)
@@ -18,8 +31,7 @@ export { getProviderLabel }
function isProviderConfigured(providerId: string, config: ApiConfiguration): boolean {
switch (providerId) {
case "cline":
// Check if user has Cline API key or Cline account auth data stored
return !!(config.clineApiKey ?? config["cline:clineAccountId"])
return true // Always available
case "anthropic":
return !!config.apiKey
case "openrouter":
@@ -33,8 +45,7 @@ function isProviderConfigured(providerId: string, config: ApiConfiguration): boo
case "openai-native":
return !!config.openAiNativeApiKey
case "openai-codex":
// OpenAI Codex uses OAuth with credentials stored as JSON blob
return !!(config as Record<string, unknown>)["openai-codex-oauth-credentials"]
return !!config.openAiCodexRefreshToken
case "deepseek":
return !!config.deepSeekApiKey
case "xai":
@@ -125,16 +136,18 @@ interface ProviderPickerProps {
export const ProviderPicker: React.FC<ProviderPickerProps> = ({ onSelect, isActive = true }) => {
// Get API configuration to check which providers are configured
const apiConfig = StateManager.get().getApiConfiguration()
const sorted = useValidProviders()
// Use providers.json order, filtered to exclude CLI-incompatible providers
// Use providers.json order, filtered to only available providers
const items: SearchableListItem[] = useMemo(() => {
return sorted.map((providerId: string) => ({
const availableProviders = new Set(API_PROVIDERS_LIST)
const sorted = providerOrder.filter((p) => availableProviders.has(p))
return sorted.map((providerId) => ({
id: providerId,
label: getProviderLabel(providerId),
suffix: isProviderConfigured(providerId, apiConfig) ? "(Configured)" : undefined,
}))
}, [apiConfig, sorted])
}, [apiConfig])
return <SearchableList isActive={isActive} items={items} onSelect={(item) => onSelect(item.id)} />
}
@@ -4,8 +4,7 @@
*/
import { Box, Text, useInput } from "ink"
// biome-ignore lint/correctness/noUnusedImports: React is needed for JSX transform (tsconfig uses jsx: react)
import React, { useState } from "react"
import { useState } from "react"
import { COLORS } from "../constants/colors"
import { useStdinContext } from "../context/StdinContext"
@@ -5,41 +5,26 @@
import type { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import type { ApiProvider, ModelInfo } from "@shared/api"
import { getProviderModelIdKey, isSettingsKey, ProviderToApiKeyMap } from "@shared/storage"
import { isOpenaiReasoningEffort, OPENAI_REASONING_EFFORT_OPTIONS, type OpenaiReasoningEffort } from "@shared/storage/types"
import { ProviderToApiKeyMap } from "@shared/storage"
import type { TelemetrySetting } from "@shared/TelemetrySetting"
import { Box, Text, useInput } from "ink"
import Spinner from "ink-spinner"
import React, { useCallback, useEffect, useMemo, useState } from "react"
import { buildApiHandler } from "@/core/api"
import type { Controller } from "@/core/controller"
import { refreshOcaModels } from "@/core/controller/models/refreshOcaModels"
import { StateManager } from "@/core/storage/StateManager"
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
import { ClineAccountService } from "@/services/account/ClineAccountService"
import { AuthService, ClineAccountOrganization } from "@/services/auth/AuthService"
import { StringRequest } from "@/shared/proto/cline/common"
import { openExternal } from "@/utils/env"
import { supportsReasoningEffortForModel } from "@/utils/model-utils"
import { version as CLI_VERSION } from "../../package.json"
import { COLORS } from "../constants/colors"
import { useStdinContext } from "../context/StdinContext"
import { useOcaAuth } from "../hooks/useOcaAuth"
import { isMouseEscapeSequence } from "../utils/input"
import { applyBedrockConfig, applyProviderConfig } from "../utils/provider-config"
import { applyProviderConfig } from "../utils/provider-config"
import { ApiKeyInput } from "./ApiKeyInput"
import { type BedrockConfig, BedrockSetup } from "./BedrockSetup"
import { Checkbox } from "./Checkbox"
import {
FeaturedModelPicker,
getFeaturedModelAtIndex,
getFeaturedModelMaxIndex,
isBrowseAllSelected,
} from "./FeaturedModelPicker"
import { LanguagePicker } from "./LanguagePicker"
import { hasModelPicker, ModelPicker } from "./ModelPicker"
import { OcaEmployeeCheck } from "./OcaEmployeeCheck"
import { getDefaultModelId, hasModelPicker, ModelPicker } from "./ModelPicker"
import { OrganizationPicker } from "./OrganizationPicker"
import { Panel, PanelTab } from "./Panel"
import { getProviderLabel, ProviderPicker } from "./ProviderPicker"
@@ -47,8 +32,7 @@ import { getProviderLabel, ProviderPicker } from "./ProviderPicker"
interface SettingsPanelContentProps {
onClose: () => void
controller?: Controller
initialMode?: "model-picker" | "featured-models"
initialModelKey?: "actModelId" | "planModelId"
initialMode?: "model-picker"
}
type SettingsTab = "api" | "auto-approve" | "features" | "other" | "account"
@@ -56,25 +40,13 @@ type SettingsTab = "api" | "auto-approve" | "features" | "other" | "account"
interface ListItem {
key: string
label: string
type: "checkbox" | "readonly" | "editable" | "separator" | "header" | "spacer" | "action" | "cycle"
type: "checkbox" | "readonly" | "editable" | "separator" | "header" | "spacer" | "action"
value: string | boolean
description?: string
isSubItem?: boolean
parentKey?: string
}
function normalizeReasoningEffort(value: unknown): OpenaiReasoningEffort {
if (isOpenaiReasoningEffort(value)) {
return value
}
return "low"
}
function nextReasoningEffort(current: OpenaiReasoningEffort): OpenaiReasoningEffort {
const idx = OPENAI_REASONING_EFFORT_OPTIONS.indexOf(current)
return OPENAI_REASONING_EFFORT_OPTIONS[(idx + 1) % OPENAI_REASONING_EFFORT_OPTIONS.length]
}
const TABS: PanelTab[] = [
{ key: "api", label: "API" },
{ key: "auto-approve", label: "Auto-approve" },
@@ -85,47 +57,47 @@ const TABS: PanelTab[] = [
// Settings configuration for simple boolean toggles
const FEATURE_SETTINGS = {
subagents: {
stateKey: "subagentsEnabled",
default: false,
label: "Subagents",
description: "Let Cline run focused subagents in parallel to explore the codebase for you",
},
autoCondense: {
stateKey: "useAutoCondense",
stateKey: "useAutoCondense" as const,
default: false,
label: "Auto-condense",
description: "Automatically summarize long conversations",
},
webTools: {
stateKey: "clineWebToolsEnabled",
stateKey: "clineWebToolsEnabled" as const,
default: true,
label: "Web tools",
description: "Enable web search and fetch tools",
},
strictPlanMode: {
stateKey: "strictPlanModeEnabled",
stateKey: "strictPlanModeEnabled" as const,
default: true,
label: "Strict plan mode",
description: "Require explicit mode switching",
},
nativeToolCall: {
stateKey: "nativeToolCallEnabled",
stateKey: "nativeToolCallEnabled" as const,
default: true,
label: "Native tool call",
description: "Use model's native tool calling API",
},
parallelToolCalling: {
stateKey: "enableParallelToolCalling",
stateKey: "enableParallelToolCalling" as const,
default: false,
label: "Parallel tool calling",
description: "Allow multiple tools in a single response",
},
doubleCheckCompletion: {
stateKey: "doubleCheckCompletionEnabled",
skillsEnabled: {
stateKey: "skillsEnabled" as const,
default: false,
label: "Double-check completion",
description: "Reject first completion attempt and require re-verification",
label: "Skills",
description: "Enable reusable agent instructions",
},
autoApproveAll: {
stateKey: "yoloModeToggled" as const,
default: false,
label: "Auto-approve all",
description: "Automatically approve all actions (Shift+Tab)",
},
} as const
@@ -141,12 +113,7 @@ function formatBalance(balance: number | null): string {
return `$${(balance / 1000000).toFixed(2)}`
}
export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
onClose,
controller,
initialMode,
initialModelKey,
}) => {
export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({ onClose, controller, initialMode }) => {
const { isRawModeSupported } = useStdinContext()
const stateManager = StateManager.get()
@@ -156,16 +123,13 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
const [isEditing, setIsEditing] = useState(false)
const [isPickingModel, setIsPickingModel] = useState(initialMode === "model-picker")
const [pickingModelKey, setPickingModelKey] = useState<"actModelId" | "planModelId" | null>(
initialMode ? (initialModelKey ?? "actModelId") : null,
initialMode === "model-picker" ? "actModelId" : null,
)
const [isPickingFeaturedModel, setIsPickingFeaturedModel] = useState(initialMode === "featured-models")
const [featuredModelIndex, setFeaturedModelIndex] = useState(0)
const [isPickingProvider, setIsPickingProvider] = useState(false)
const [isPickingLanguage, setIsPickingLanguage] = useState(false)
const [isEnteringApiKey, setIsEnteringApiKey] = useState(false)
const [isConfiguringBedrock, setIsConfiguringBedrock] = useState(false)
const [isWaitingForCodexAuth, setIsWaitingForCodexAuth] = useState(false)
const [isShowingOcaEmployeeCheck, setIsShowingOcaEmployeeCheck] = useState(false)
const [codexAuthError, setCodexAuthError] = useState<string | null>(null)
const [pendingProvider, setPendingProvider] = useState<string | null>(null)
const [apiKeyValue, setApiKeyValue] = useState("")
@@ -175,11 +139,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
const [features, setFeatures] = useState<Record<FeatureKey, boolean>>(() => {
const initial: Record<string, boolean> = {}
for (const [key, config] of Object.entries(FEATURE_SETTINGS)) {
if (isSettingsKey(config.stateKey)) {
initial[key] = stateManager.getGlobalSettingsKey(config.stateKey)
} else {
initial[key] = stateManager.getGlobalStateKey(config.stateKey)
}
initial[key] = stateManager.getGlobalSettingsKey(config.stateKey) ?? config.default
}
return initial as Record<FeatureKey, boolean>
})
@@ -195,12 +155,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
const [planThinkingEnabled, setPlanThinkingEnabled] = useState<boolean>(
() => (stateManager.getGlobalSettingsKey("planModeThinkingBudgetTokens") ?? 0) > 0,
)
const [actReasoningEffort, setActReasoningEffort] = useState<OpenaiReasoningEffort>(() =>
normalizeReasoningEffort(stateManager.getGlobalSettingsKey("actModeReasoningEffort")),
)
const [planReasoningEffort, setPlanReasoningEffort] = useState<OpenaiReasoningEffort>(() =>
normalizeReasoningEffort(stateManager.getGlobalSettingsKey("planModeReasoningEffort")),
)
// Auto-approve settings (complex nested object)
const [autoApproveSettings, setAutoApproveSettings] = useState<AutoApprovalSettings>(() => {
@@ -226,50 +180,12 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
const [accountChecked, setAccountChecked] = useState(false) // Tracks if we've already checked auth
// Get current provider and model info
const apiConfig = stateManager.getApiConfiguration()
const [provider, setProvider] = useState<string>(
() =>
stateManager.getApiConfiguration().actModeApiProvider ||
stateManager.getApiConfiguration().planModeApiProvider ||
"not configured",
() => apiConfig.actModeApiProvider || apiConfig.planModeApiProvider || "not configured",
)
// Refresh trigger to force re-reading model IDs from state
const [modelRefreshKey, setModelRefreshKey] = useState(0)
const refreshModelIds = useCallback(() => setModelRefreshKey((k) => k + 1), [])
// OCA auth hook
const handleOcaAuthSuccess = useCallback(async () => {
await applyProviderConfig({ providerId: "oca", controller })
// Fetch OCA models from the API - this sets actModeOcaModelId/planModeOcaModelId in state
await refreshOcaModels(controller!, StringRequest.create({ value: "" }))
setProvider("oca")
refreshModelIds()
}, [controller, refreshModelIds])
const {
isWaiting: isWaitingForOcaAuth,
startAuth: startOcaAuth,
cancelAuth: cancelOcaAuth,
isAuthenticated: isOcaAuthenticated,
} = useOcaAuth({
controller,
onSuccess: handleOcaAuthSuccess,
})
// Read model IDs from state (re-reads when refreshKey changes)
const { actModelId, planModelId } = useMemo(() => {
const apiConfig = stateManager.getApiConfiguration()
const actProvider = apiConfig.actModeApiProvider
const planProvider = apiConfig.planModeApiProvider || actProvider
if (!actProvider && !planProvider) {
return { actModelId: "", planModelId: "" }
}
const actKey = actProvider ? getProviderModelIdKey(actProvider, "act") : null
const planKey = planProvider ? getProviderModelIdKey(planProvider, "plan") : null
return {
actModelId: actKey ? (stateManager.getGlobalSettingsKey(actKey) as string) || "" : "",
planModelId: planKey ? (stateManager.getGlobalSettingsKey(planKey) as string) || "" : "",
}
}, [modelRefreshKey, stateManager])
const actModelId = (stateManager.getGlobalSettingsKey("actModeApiModelId") as string) || ""
const planModelId = (stateManager.getGlobalSettingsKey("planModeApiModelId") as string) || ""
// Toggle a feature setting
const toggleFeature = useCallback(
@@ -311,21 +227,19 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
return
}
const accountService = ClineAccountService.getInstance()
// Fetch fresh organization info from server (like webview's getUserOrganizations RPC)
// Don't use authService.getUserOrganizations() as it returns cached data
const organizations = await accountService.fetchUserOrganizationsRPC()
let activeOrgId: string | undefined
// Get organization info
const organizations = authService.getUserOrganizations()
if (organizations) {
setAccountOrganizations(organizations)
const activeOrg = organizations.find((org) => org.active)
setAccountOrganization(activeOrg || null)
activeOrgId = activeOrg?.organizationId
}
// Fetch credit balance
try {
const accountService = ClineAccountService.getInstance()
const activeOrgId = authService.getActiveOrganizationId()
if (activeOrgId) {
const orgBalance = await accountService.fetchOrganizationCreditsRPC(activeOrgId)
if (orgBalance?.balance !== undefined) {
@@ -385,8 +299,9 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
setIsPickingOrganization(false)
try {
await ClineAccountService.getInstance().switchAccount(orgId || undefined)
// Refetch fresh org data from server
await fetchAccountInfo()
// Refetch to get updated auth info with new active org
await AuthService.getInstance(controller).restoreRefreshTokenAndRetrieveAuthInfo()
fetchAccountInfo()
} catch {
// Error switching organization
}
@@ -419,7 +334,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
setAccountChecked(false) // Reset so fetchAccountInfo can run
await applyProviderConfig({ providerId: "cline", controller })
setProvider("cline")
refreshModelIds()
fetchAccountInfo()
}
}
@@ -433,12 +347,9 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
// Build items list based on current tab
const items: ListItem[] = useMemo(() => {
// Some providers/models expose reasoning effort instead of thinking budget controls.
const providerUsesReasoningEffort = provider === "openai-native" || provider === "openai-codex"
const showActReasoningEffort = supportsReasoningEffortForModel(actModelId || "")
const showPlanReasoningEffort = supportsReasoningEffortForModel(planModelId || "")
const showActThinkingOption = !providerUsesReasoningEffort && !showActReasoningEffort
const showPlanThinkingOption = !providerUsesReasoningEffort && !showPlanReasoningEffort
// OpenAI Native, Codex, and GPT models don't support thinking budget (they use reasoning effort)
const isGptModel = actModelId?.toLowerCase().includes("gpt") || planModelId?.toLowerCase().includes("gpt")
const showThinkingOption = provider !== "openai-native" && provider !== "openai-codex" && !isGptModel
switch (currentTab) {
case "api":
@@ -462,7 +373,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
type: "editable" as const,
value: actModelId || "not set",
},
...(showActThinkingOption
...(showThinkingOption
? [
{
key: "actThinkingEnabled",
@@ -472,16 +383,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
},
]
: []),
...(showActReasoningEffort
? [
{
key: "actReasoningEffort",
label: "Reasoning effort",
type: "cycle" as const,
value: actReasoningEffort,
},
]
: []),
{ key: "planHeader", label: "Plan Mode", type: "header" as const, value: "" },
{
key: "planModelId",
@@ -489,7 +390,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
type: "editable" as const,
value: planModelId || "not set",
},
...(showPlanThinkingOption
...(showThinkingOption
? [
{
key: "planThinkingEnabled",
@@ -499,16 +400,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
},
]
: []),
...(showPlanReasoningEffort
? [
{
key: "planReasoningEffort",
label: "Reasoning effort",
type: "cycle" as const,
value: planReasoningEffort,
},
]
: []),
{ key: "spacer1", label: "", type: "spacer" as const, value: "" },
]
: [
@@ -518,7 +409,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
type: "editable" as const,
value: actModelId || "not set",
},
...(showActThinkingOption
...(showThinkingOption
? [
{
key: "actThinkingEnabled",
@@ -528,16 +419,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
},
]
: []),
...(showActReasoningEffort
? [
{
key: "actReasoningEffort",
label: "Reasoning effort",
type: "cycle" as const,
value: actReasoningEffort,
},
]
: []),
]),
{
key: "separateModels",
@@ -564,7 +445,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
key: parentKey,
label: parentLabel,
type: "checkbox",
value: actions[parentKey as keyof typeof actions] ?? false,
value: actions[parentKey as keyof typeof actions],
description: parentDesc,
})
if (actions[parentKey as keyof typeof actions]) {
@@ -648,11 +529,9 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
key: "telemetry",
label: "Error/usage reporting",
type: "checkbox",
value: telemetry !== "disabled",
value: telemetry === "enabled",
description: "Help improve Cline by sending anonymous usage data",
},
{ key: "separator", label: "", type: "separator", value: "" },
{ key: "version", label: "", type: "readonly", value: `Cline v${CLI_VERSION}` },
]
case "account":
@@ -700,8 +579,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
separateModels,
actThinkingEnabled,
planThinkingEnabled,
actReasoningEffort,
planReasoningEffort,
autoApproveSettings,
features,
preferredLanguage,
@@ -735,33 +612,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
}
}, [items.length, selectedIndex])
const rebuildTaskApi = useCallback(() => {
if (!controller?.task) {
return
}
const currentMode = stateManager.getGlobalSettingsKey("mode")
const apiConfig = stateManager.getApiConfiguration()
controller.task.api = buildApiHandler({ ...apiConfig, ulid: controller.task.ulid }, currentMode)
}, [controller, stateManager])
const setReasoningEffortForMode = useCallback(
(mode: "act" | "plan", effort: OpenaiReasoningEffort) => {
if (mode === "act") {
setActReasoningEffort(effort)
stateManager.setGlobalState("actModeReasoningEffort", effort)
if (!separateModels) {
setPlanReasoningEffort(effort)
stateManager.setGlobalState("planModeReasoningEffort", effort)
}
} else {
setPlanReasoningEffort(effort)
stateManager.setGlobalState("planModeReasoningEffort", effort)
}
rebuildTaskApi()
},
[separateModels, rebuildTaskApi, stateManager],
)
// Handle toggle/edit for selected item
const handleAction = useCallback(() => {
const item = items[selectedIndex]
@@ -785,15 +635,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
return
}
if (item.type === "cycle") {
const targetMode = item.key === "actReasoningEffort" ? "act" : item.key === "planReasoningEffort" ? "plan" : undefined
if (targetMode) {
const currentEffort = targetMode === "act" ? actReasoningEffort : planReasoningEffort
setReasoningEffortForMode(targetMode, nextReasoningEffort(currentEffort))
}
return
}
if (item.type === "editable") {
// For provider field, use the provider picker
if (item.key === "provider") {
@@ -803,13 +644,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
// For model ID fields, check if we should use the model picker
if ((item.key === "actModelId" || item.key === "planModelId") && hasModelPicker(provider)) {
setPickingModelKey(item.key as "actModelId" | "planModelId")
// For Cline provider, show featured models first
if (provider === "cline") {
setFeaturedModelIndex(0)
setIsPickingFeaturedModel(true)
} else {
setIsPickingModel(true)
}
setIsPickingModel(true)
return
}
// For language field, use the language picker
@@ -842,25 +677,9 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
stateManager.setGlobalState("planActSeparateModelsSetting", newValue)
// When disabling separate models, sync plan model to act model
if (!newValue) {
const apiConfig = stateManager.getApiConfiguration()
const actProvider = apiConfig.actModeApiProvider
const planProvider = apiConfig.planModeApiProvider || actProvider
if (actProvider) {
const actKey = getProviderModelIdKey(actProvider, "act")
const planKey = planProvider ? getProviderModelIdKey(planProvider, "plan") : null
const actModel = stateManager.getGlobalSettingsKey(actKey)
if (planKey) stateManager.setGlobalState(planKey, actModel)
}
const actThinkingBudget = stateManager.getGlobalSettingsKey("actModeThinkingBudgetTokens") ?? 0
stateManager.setGlobalState("planModeThinkingBudgetTokens", actThinkingBudget)
setPlanThinkingEnabled(actThinkingBudget > 0)
const actEffort = normalizeReasoningEffort(stateManager.getGlobalSettingsKey("actModeReasoningEffort"))
stateManager.setGlobalState("planModeReasoningEffort", actEffort)
setPlanReasoningEffort(actEffort)
const actModel = stateManager.getGlobalSettingsKey("actModeApiModelId")
stateManager.setGlobalState("planModeApiModelId", actModel)
}
rebuildTaskApi()
return
}
@@ -868,19 +687,23 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
if (item.key === "actThinkingEnabled") {
setActThinkingEnabled(newValue)
stateManager.setGlobalState("actModeThinkingBudgetTokens", newValue ? 1024 : 0)
if (!separateModels) {
setPlanThinkingEnabled(newValue)
stateManager.setGlobalState("planModeThinkingBudgetTokens", newValue ? 1024 : 0)
}
// Rebuild API handler to apply thinking budget change
rebuildTaskApi()
if (controller?.task) {
const currentMode = stateManager.getGlobalSettingsKey("mode")
const apiConfig = stateManager.getApiConfiguration()
controller.task.api = buildApiHandler({ ...apiConfig, ulid: controller.task.ulid }, currentMode)
}
return
}
if (item.key === "planThinkingEnabled") {
setPlanThinkingEnabled(newValue)
stateManager.setGlobalState("planModeThinkingBudgetTokens", newValue ? 1024 : 0)
// Rebuild API handler to apply thinking budget change
rebuildTaskApi()
if (controller?.task) {
const currentMode = stateManager.getGlobalSettingsKey("mode")
const apiConfig = stateManager.getApiConfiguration()
controller.task.api = buildApiHandler({ ...apiConfig, ulid: controller.task.ulid }, currentMode)
}
return
}
@@ -889,11 +712,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
const newTelemetry: TelemetrySetting = newValue ? "enabled" : "disabled"
setTelemetry(newTelemetry)
stateManager.setGlobalState("telemetrySetting", newTelemetry)
// Flush synchronously before continuing - must complete before app can exit
void stateManager.flushPendingState().then(() => {
// Update telemetry providers to respect the new setting
controller?.updateTelemetrySetting(newTelemetry)
})
return
}
@@ -937,78 +755,25 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
handleClineLogin,
handleClineLogout,
accountOrganizations,
separateModels,
actReasoningEffort,
planReasoningEffort,
rebuildTaskApi,
setReasoningEffortForMode,
])
// Handle model selection from picker
const handleModelSelect = useCallback(
async (modelId: string) => {
(modelId: string) => {
if (!pickingModelKey) return
const apiConfig = stateManager.getApiConfiguration()
const actProvider = apiConfig.actModeApiProvider
const planProvider = apiConfig.planModeApiProvider || actProvider
const providerForSelection = separateModels
? pickingModelKey === "actModelId"
? actProvider
: planProvider
: actProvider || planProvider
if (!providerForSelection) return
// Use provider-specific model ID keys (e.g., cline uses actModeOpenRouterModelId)
const actKey = actProvider ? getProviderModelIdKey(actProvider, "act") : null
const planKey = planProvider ? getProviderModelIdKey(planProvider, "plan") : null
// For cline/openrouter providers, also set model info (like webview does)
let modelInfo: ModelInfo | undefined
if (providerForSelection === "cline" || providerForSelection === "openrouter") {
const openRouterModels = await controller?.readOpenRouterModels()
modelInfo = openRouterModels?.[modelId]
}
if (separateModels) {
// Only update the selected mode's model
const stateKey = pickingModelKey === "actModelId" ? actKey : planKey
if (stateKey) stateManager.setGlobalState(stateKey, modelId)
// Set model info for the selected mode
if (modelInfo) {
const infoKey =
pickingModelKey === "actModelId" ? "actModeOpenRouterModelInfo" : "planModeOpenRouterModelInfo"
stateManager.setGlobalState(infoKey, modelInfo)
}
const stateKey = pickingModelKey === "actModelId" ? "actModeApiModelId" : "planModeApiModelId"
stateManager.setGlobalState(stateKey, modelId)
} else {
// Update both modes to keep them in sync
if (actKey) stateManager.setGlobalState(actKey, modelId)
if (planKey) stateManager.setGlobalState(planKey, modelId)
// Set model info for both modes
if (modelInfo) {
stateManager.setGlobalState("actModeOpenRouterModelInfo", modelInfo)
stateManager.setGlobalState("planModeOpenRouterModelInfo", modelInfo)
}
stateManager.setGlobalState("actModeApiModelId", modelId)
stateManager.setGlobalState("planModeApiModelId", modelId)
}
// Flush pending state to ensure model ID is persisted
await stateManager.flushPendingState()
// Rebuild API handler if there's an active task
if (controller?.task) {
const currentMode = stateManager.getGlobalSettingsKey("mode")
const freshApiConfig = stateManager.getApiConfiguration()
controller.task.api = buildApiHandler({ ...freshApiConfig, ulid: controller.task.ulid }, currentMode)
}
refreshModelIds()
setIsPickingModel(false)
setPickingModelKey(null)
// If opened from /models command, close the entire settings panel
if (initialMode) {
onClose()
}
},
[pickingModelKey, separateModels, stateManager, controller, refreshModelIds, initialMode, onClose],
[pickingModelKey, separateModels, stateManager],
)
// Handle language selection from picker
@@ -1039,7 +804,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
// Success - apply provider config
await applyProviderConfig({ providerId: "openai-codex", controller })
setProvider("openai-codex")
refreshModelIds()
setIsWaitingForCodexAuth(false)
} catch (error) {
openAiCodexOAuthManager.cancelAuthorizationFlow()
@@ -1048,8 +812,9 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
}
}, [controller])
// Handle provider selection from picker
const handleProviderSelect = useCallback(
async (providerId: string) => {
(providerId: string) => {
// Special handling for Cline - uses OAuth (but skip if already logged in)
if (providerId === "cline") {
setIsPickingProvider(false)
@@ -1057,9 +822,8 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
const authInfo = AuthService.getInstance(controller).getInfo()
if (authInfo?.user?.email) {
// Already logged in - just set the provider
await applyProviderConfig({ providerId: "cline", controller })
applyProviderConfig({ providerId: "cline", controller })
setProvider("cline")
refreshModelIds()
} else {
// Not logged in - trigger OAuth
handleClineLogin()
@@ -1074,22 +838,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
return
}
// Special handling for OCA - uses OAuth (but skip if already logged in)
if (providerId === "oca") {
setIsPickingProvider(false)
// Check if already logged in
if (isOcaAuthenticated) {
// Already logged in - just set the provider
await applyProviderConfig({ providerId: "oca", controller })
setProvider("oca")
refreshModelIds()
} else {
// Not logged in - show employee check before auth
setIsShowingOcaEmployeeCheck(true)
}
return
}
// Special handling for Bedrock - needs multi-field configuration
if (providerId === "bedrock") {
setPendingProvider(providerId)
@@ -1099,7 +847,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
}
// Check if this provider needs an API key
const keyField = ProviderToApiKeyMap[providerId as ApiProvider]
const keyField = ProviderToApiKeyMap[providerId as keyof typeof ProviderToApiKeyMap]
if (keyField) {
// Provider needs an API key - go to API key entry mode
// Pre-fill with existing key if configured
@@ -1112,13 +860,12 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
setIsEnteringApiKey(true)
} else {
// Provider doesn't need an API key (rare) - just set it
await applyProviderConfig({ providerId, controller })
applyProviderConfig({ providerId, controller })
setProvider(providerId)
refreshModelIds()
setIsPickingProvider(false)
}
},
[stateManager, startCodexAuth, handleClineLogin, startOcaAuth, isOcaAuthenticated, controller, refreshModelIds],
[stateManager, startCodexAuth, handleClineLogin, controller],
)
// Handle API key submission after provider selection
@@ -1130,27 +877,53 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
await applyProviderConfig({ providerId: pendingProvider, apiKey: submittedValue.trim(), controller })
setProvider(pendingProvider)
refreshModelIds()
setIsEnteringApiKey(false)
setPendingProvider(null)
setApiKeyValue("")
},
[pendingProvider, controller, refreshModelIds],
[pendingProvider, controller],
)
// Handle Bedrock configuration complete
const handleBedrockComplete = useCallback(
(bedrockConfig: BedrockConfig) => {
// Update UI state first for responsiveness
const config: Record<string, unknown> = {
actModeApiProvider: "bedrock",
planModeApiProvider: "bedrock",
apiProvider: "bedrock",
awsAuthentication: bedrockConfig.awsAuthentication,
awsRegion: bedrockConfig.awsRegion,
awsUseCrossRegionInference: bedrockConfig.awsUseCrossRegionInference,
}
const defaultModelId = getDefaultModelId("bedrock")
if (defaultModelId) {
config.actModeApiModelId = defaultModelId
config.planModeApiModelId = defaultModelId
}
if (bedrockConfig.awsProfile !== undefined) config.awsProfile = bedrockConfig.awsProfile
if (bedrockConfig.awsAccessKey) config.awsAccessKey = bedrockConfig.awsAccessKey
if (bedrockConfig.awsSecretKey) config.awsSecretKey = bedrockConfig.awsSecretKey
if (bedrockConfig.awsSessionToken) config.awsSessionToken = bedrockConfig.awsSessionToken
stateManager.setApiConfiguration(config as Record<string, string>)
// Close Bedrock config first, then flush state async
setProvider("bedrock")
refreshModelIds()
setIsConfiguringBedrock(false)
setPendingProvider(null)
// Apply config and rebuild API handler in background
applyBedrockConfig({ bedrockConfig, controller })
// Flush state and rebuild API handler in background
stateManager.flushPendingState().then(() => {
if (controller?.task) {
const currentMode = stateManager.getGlobalSettingsKey("mode")
const apiConfig = stateManager.getApiConfiguration()
controller.task.api = buildApiHandler({ ...apiConfig, ulid: controller.task.ulid }, currentMode)
}
})
},
[controller, refreshModelIds],
[stateManager, controller],
)
// Handle saving edited value
@@ -1160,26 +933,17 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
switch (item.key) {
case "actModelId":
case "planModelId": {
// Use provider-specific model ID keys (e.g., cline uses actModeOpenRouterModelId)
const apiConfig = stateManager.getApiConfiguration()
const actProvider = apiConfig.actModeApiProvider
const planProvider = apiConfig.planModeApiProvider || actProvider
if (!actProvider && !planProvider) break
const actKey = actProvider ? getProviderModelIdKey(actProvider, "act") : null
const planKey = planProvider ? getProviderModelIdKey(planProvider, "plan") : null
case "planModelId":
if (separateModels) {
// Only update the selected mode's model
const stateKey = item.key === "actModelId" ? actKey : planKey
if (stateKey) stateManager.setGlobalState(stateKey, editValue || undefined)
const stateKey = item.key === "actModelId" ? "actModeApiModelId" : "planModeApiModelId"
stateManager.setGlobalState(stateKey, editValue || undefined)
} else {
// Update both modes to keep them in sync
if (actKey) stateManager.setGlobalState(actKey, editValue || undefined)
if (planKey) stateManager.setGlobalState(planKey, editValue || undefined)
stateManager.setGlobalState("actModeApiModelId", editValue || undefined)
stateManager.setGlobalState("planModeApiModelId", editValue || undefined)
}
break
}
case "language":
setPreferredLanguage(editValue)
stateManager.setGlobalState("preferredLanguage", editValue)
@@ -1240,47 +1004,11 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
return
}
// Featured model picker mode (Cline provider)
if (isPickingFeaturedModel) {
const maxIndex = getFeaturedModelMaxIndex()
if (key.escape) {
setIsPickingFeaturedModel(false)
setPickingModelKey(null)
// If opened from /models command, close the entire settings panel
if (initialMode) {
onClose()
}
} else if (key.upArrow) {
setFeaturedModelIndex((prev) => (prev > 0 ? prev - 1 : maxIndex))
} else if (key.downArrow) {
setFeaturedModelIndex((prev) => (prev < maxIndex ? prev + 1 : 0))
} else if (key.return) {
if (isBrowseAllSelected(featuredModelIndex)) {
// Switch to full ModelPicker
setIsPickingFeaturedModel(false)
setIsPickingModel(true)
} else {
const selectedModel = getFeaturedModelAtIndex(featuredModelIndex)
if (selectedModel && pickingModelKey) {
handleModelSelect(selectedModel.id)
setIsPickingFeaturedModel(false)
setPickingModelKey(null)
}
}
}
return
}
// Model picker mode - escape to close, input is handled by ModelPicker
if (isPickingModel) {
if (key.escape) {
setIsPickingModel(false)
setPickingModelKey(null)
// If opened from /models command, close the entire settings panel
if (initialMode) {
onClose()
}
}
return
}
@@ -1324,14 +1052,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
return
}
// OCA OAuth waiting mode - escape to cancel
if (isWaitingForOcaAuth) {
if (key.escape) {
cancelOcaAuth()
}
return
}
if (isEditing) {
if (key.escape) {
setIsEditing(false)
@@ -1376,7 +1096,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
return
}
},
{ isActive: isRawModeSupported && !isEnteringApiKey && !isConfiguringBedrock && !isShowingOcaEmployeeCheck },
{ isActive: isRawModeSupported && !isEnteringApiKey && !isConfiguringBedrock },
)
// Render content
@@ -1463,17 +1183,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
)
}
if (isPickingFeaturedModel && pickingModelKey) {
const label = pickingModelKey === "actModelId" ? "Model ID (Act)" : "Model ID (Plan)"
return (
<FeaturedModelPicker
helpText="Arrows to navigate, Enter to select, Esc to cancel"
selectedIndex={featuredModelIndex}
title={`Select: ${label}`}
/>
)
}
if (isPickingModel && pickingModelKey) {
const label = pickingModelKey === "actModelId" ? "Model ID (Act)" : "Model ID (Plan)"
return (
@@ -1552,38 +1261,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
)
}
if (isShowingOcaEmployeeCheck) {
return (
<OcaEmployeeCheck
isActive={isShowingOcaEmployeeCheck}
onCancel={() => setIsShowingOcaEmployeeCheck(false)}
onSignIn={() => {
setIsShowingOcaEmployeeCheck(false)
startOcaAuth()
}}
/>
)
}
if (isWaitingForOcaAuth) {
return (
<Box flexDirection="column">
<Box>
<Text color={COLORS.primaryBlue}>
<Spinner type="dots" />
</Text>
<Text color="white"> Waiting for OCA sign-in...</Text>
</Box>
<Box marginTop={1}>
<Text color="gray">Complete sign-in in your browser.</Text>
</Box>
<Box marginTop={1}>
<Text color="gray">Esc to cancel</Text>
</Box>
</Box>
)
}
// Account tab - loading state
if (currentTab === "account" && isAccountLoading) {
return (
@@ -1640,8 +1317,26 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
)
}
// Show special message for auto-approve tab when auto-approve all is enabled
if (currentTab === "auto-approve" && features.autoApproveAll) {
return (
<Box flexDirection="column">
<Text color="green">Auto-approve all is enabled (Shift+Tab)</Text>
<Box marginTop={1}>
<Text color="gray">All tool calls are automatically approved. When disabled, the settings</Text>
</Box>
<Text color="gray">below control which actions are auto-approved.</Text>
</Box>
)
}
return (
<Box flexDirection="column">
{currentTab === "auto-approve" && !features.autoApproveAll && (
<Box marginBottom={1}>
<Text color="gray">These actions are auto-approved when 'Auto-approve all' is disabled:</Text>
</Box>
)}
{items.map((item, idx) => {
const isSelected = idx === selectedIndex
@@ -1701,28 +1396,13 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
)
}
if (item.type === "cycle") {
return (
<Text key={item.key}>
<Text bold color={isSelected ? COLORS.primaryBlue : undefined}>
{isSelected ? "" : " "}{" "}
</Text>
<Text color={isSelected ? COLORS.primaryBlue : "white"}>{item.label}: </Text>
<Text color={COLORS.primaryBlue}>
{typeof item.value === "string" ? item.value : String(item.value)}
</Text>
{isSelected && <Text color="gray"> (Tab to cycle)</Text>}
</Text>
)
}
// Readonly or editable field
return (
<Text key={item.key}>
<Text bold color={isSelected ? COLORS.primaryBlue : undefined}>
{isSelected ? "" : " "}{" "}
</Text>
{item.label && <Text color={isSelected ? COLORS.primaryBlue : "white"}>{item.label}: </Text>}
<Text color={isSelected ? COLORS.primaryBlue : "white"}>{item.label}: </Text>
<Text color={item.type === "readonly" ? "gray" : COLORS.primaryBlue}>
{typeof item.value === "string" ? item.value : String(item.value)}
</Text>
@@ -1734,24 +1414,8 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
)
}
// Determine if we're in a subpage (picker, editor, or waiting state)
const isSubpage =
isPickingProvider ||
isPickingModel ||
isPickingFeaturedModel ||
isPickingLanguage ||
isEnteringApiKey ||
isConfiguringBedrock ||
isWaitingForCodexAuth ||
!!codexAuthError ||
isPickingOrganization ||
isWaitingForClineAuth ||
isShowingOcaEmployeeCheck ||
isWaitingForOcaAuth ||
isEditing
return (
<Panel currentTab={currentTab} isSubpage={isSubpage} label="Settings" tabs={TABS}>
<Panel currentTab={currentTab} label="Settings" tabs={TABS}>
{renderContent()}
</Panel>
)
@@ -7,7 +7,6 @@ import type { SlashCommandInfo } from "@shared/proto/cline/slash"
import { Box, Text } from "ink"
import React from "react"
import { COLORS } from "../constants/colors"
import { useTerminalSize } from "../hooks/useTerminalSize"
import { getVisibleWindow } from "../utils/slash-commands"
interface SlashCommandMenuProps {
@@ -17,16 +16,6 @@ interface SlashCommandMenuProps {
}
export const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ commands, selectedIndex, query }) => {
const { columns: terminalWidth } = useTerminalSize()
const contentWidth = Math.max(10, terminalWidth - 4)
const truncateText = (text: string, maxLength: number): string => {
if (maxLength <= 0) return ""
if (text.length <= maxLength) return text
if (maxLength <= 3) return text.slice(0, maxLength)
return text.slice(0, maxLength - 3) + "..."
}
if (commands.length === 0) {
return (
<Box flexDirection="column" marginBottom={1} paddingLeft={1} paddingRight={1}>
@@ -44,17 +33,19 @@ export const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ commands, se
const isSelected = startIndex + idx === selectedIndex
// Only show description for default commands (not workflows)
const showDescription = cmd.section === "default" || !cmd.section
const commandPrefix = `${isSelected ? "" : " "} /${cmd.name}`
const truncatedCommand = truncateText(commandPrefix, contentWidth)
const descriptionText = showDescription && cmd.description ? ` - ${cmd.description}` : ""
const fullLine = truncateText(truncatedCommand + descriptionText, contentWidth)
const commandText = fullLine.slice(0, Math.min(fullLine.length, truncatedCommand.length))
const descText = fullLine.slice(commandText.length)
return (
<Box flexWrap="nowrap" key={cmd.name} width={contentWidth}>
<Text color={isSelected ? COLORS.primaryBlue : undefined}>{commandText}</Text>
{descText && <Text color="gray">{descText}</Text>}
<Box flexDirection="column" key={cmd.name}>
<Box>
<Text color={isSelected ? COLORS.primaryBlue : undefined}>
{isSelected ? "" : " "} /{cmd.name}
</Text>
</Box>
{showDescription && cmd.description && (
<Box paddingLeft={3}>
<Text color="gray">{cmd.description}</Text>
</Box>
)}
</Box>
)
})}

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