Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/types/src/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ export interface CreateTaskOptions {
consecutiveMistakeLimit?: number
experiments?: Record<string, boolean>
initialTodos?: TodoItem[]
selectionContext?: {
selectedText: string
selectionFilePath: string
selectionStartLine: number
selectionEndLine: number
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roomote this is repeated a lot - should we move it into a type?

}

export enum TaskStatus {
Expand Down
57 changes: 57 additions & 0 deletions src/core/environment/__tests__/getEnvironmentDetails.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ describe("getEnvironmentDetails", () => {
deref: vi.fn().mockReturnValue(mockProvider),
[Symbol.toStringTag]: "WeakRef",
} as unknown as WeakRef<ClineProvider>,
getAndClearSelectionContext: vi.fn().mockReturnValue(undefined),
}

// Mock other dependencies.
Expand Down Expand Up @@ -390,4 +391,60 @@ describe("getEnvironmentDetails", () => {
const result = await getEnvironmentDetails(cline as Task)
expect(result).toContain("REMINDERS")
})

describe("Selection Context", () => {
it("should include selection context when available", async () => {
const selectionContext = {
selectedText: "const x = 1;\nconst y = 2;",
selectionFilePath: "src/test.ts",
selectionStartLine: 10,
selectionEndLine: 11,
}

const clineWithSelection = {
...mockCline,
getAndClearSelectionContext: vi.fn().mockReturnValueOnce(selectionContext),
}

const result = await getEnvironmentDetails(clineWithSelection as unknown as Task)

expect(result).toContain("# Current Selection")
expect(result).toContain("File: src/test.ts:10-11")
expect(result).toContain("```")
expect(result).toContain("const x = 1;")
expect(result).toContain("const y = 2;")
expect(clineWithSelection.getAndClearSelectionContext).toHaveBeenCalledOnce()
})

it("should clear selection context after including it", async () => {
const selectionContext = {
selectedText: "test code",
selectionFilePath: "src/app.ts",
selectionStartLine: 5,
selectionEndLine: 5,
}

const clineWithSelection = {
...mockCline,
getAndClearSelectionContext: vi.fn().mockReturnValueOnce(selectionContext),
}

await getEnvironmentDetails(clineWithSelection as unknown as Task)

// Selection context should be cleared after use (method called once)
expect(clineWithSelection.getAndClearSelectionContext).toHaveBeenCalledOnce()
})

it("should not include selection section when no context is available", async () => {
const clineWithoutSelection = {
...mockCline,
getAndClearSelectionContext: vi.fn().mockReturnValueOnce(undefined),
}

const result = await getEnvironmentDetails(clineWithoutSelection as unknown as Task)

expect(result).not.toContain("# Current Selection")
expect(clineWithoutSelection.getAndClearSelectionContext).toHaveBeenCalledOnce()
})
})
})
9 changes: 9 additions & 0 deletions src/core/environment/getEnvironmentDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
maxWorkspaceFiles = 200,
} = state ?? {}

// Include selection context if available (and automatically clear it)
const selectionContext = cline.getAndClearSelectionContext()
if (selectionContext) {
const { selectedText, selectionFilePath, selectionStartLine, selectionEndLine } = selectionContext
details += "\n\n# Current Selection"
details += `\nFile: ${selectionFilePath}:${selectionStartLine}-${selectionEndLine}`
details += `\n\`\`\`\n${selectedText}\n\`\`\``
}

// It could be useful for cline to know if the user went from one or no
// file to another between messages, so we always include this context.
details += "\n\n# VSCode Visible Files"
Expand Down
64 changes: 61 additions & 3 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ export interface TaskOptions extends CreateTaskOptions {
onCreated?: (task: Task) => void
initialTodos?: TodoItem[]
workspacePath?: string
selectionContext?: {
selectedText: string
selectionFilePath: string
selectionStartLine: number
selectionEndLine: number
}
}

export class Task extends EventEmitter<TaskEvents> implements TaskLike {
Expand All @@ -155,6 +161,15 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {

todoList?: TodoItem[]

// Temporary storage for selection context during message processing
// This is cleared after being used in getEnvironmentDetails
private _currentSelectionContext?: {
selectedText: string
selectionFilePath: string
selectionStartLine: number
selectionEndLine: number
}

readonly rootTask: Task | undefined = undefined
readonly parentTask: Task | undefined = undefined
readonly taskNumber: number
Expand Down Expand Up @@ -319,6 +334,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
onCreated,
initialTodos,
workspacePath,
selectionContext,
}: TaskOptions) {
super()

Expand Down Expand Up @@ -440,7 +456,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {

if (startTask) {
if (task || images) {
this.startTask(task, images)
this.startTask(task, images, selectionContext)
} else if (historyItem) {
this.resumeTaskFromHistory()
} else {
Expand All @@ -449,6 +465,23 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
}

/**
* Get and clear the current selection context.
* This ensures selection context is only used once and doesn't persist.
*/
public getAndClearSelectionContext():
| {
selectedText: string
selectionFilePath: string
selectionStartLine: number
selectionEndLine: number
}
| undefined {
const context = this._currentSelectionContext
this._currentSelectionContext = undefined
return context
}

/**
* Initialize the task mode from the provider state.
* This method handles async initialization with proper error handling.
Expand Down Expand Up @@ -964,11 +997,24 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
return result
}

handleWebviewAskResponse(askResponse: ClineAskResponse, text?: string, images?: string[]) {
handleWebviewAskResponse(
askResponse: ClineAskResponse,
text?: string,
images?: string[],
selectionContext?: {
selectedText: string
selectionFilePath: string
selectionStartLine: number
selectionEndLine: number
},
) {
this.askResponse = askResponse
this.askResponseText = text
this.askResponseImages = images

// Store selection context temporarily for use in the next getEnvironmentDetails call
this._currentSelectionContext = selectionContext

// Create a checkpoint whenever the user sends a message.
// Use allowEmpty=true to ensure a checkpoint is recorded even if there are no file changes.
// Suppress the checkpoint_saved chat row for this particular checkpoint to keep the timeline clean.
Expand Down Expand Up @@ -1244,7 +1290,16 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Lifecycle
// Start / Resume / Abort / Dispose

private async startTask(task?: string, images?: string[]): Promise<void> {
private async startTask(
task?: string,
images?: string[],
selectionContext?: {
selectedText: string
selectionFilePath: string
selectionStartLine: number
selectionEndLine: number
},
): Promise<void> {
if (this.enableBridge) {
try {
await BridgeOrchestrator.subscribeToTask(this)
Expand All @@ -1264,6 +1319,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
this.clineMessages = []
this.apiConversationHistory = []

// Store selection context temporarily for use in the first getEnvironmentDetails call
this._currentSelectionContext = selectionContext

// The todo list is already set in the constructor if initialTodos were provided
// No need to add any messages - the todoList property is already set

Expand Down
73 changes: 70 additions & 3 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,44 @@ export const webviewMessageHandler = async (
}

switch (message.type) {
case "requestSelectionContext": {
// Get the active editor and its selection
const editor = vscode.window.activeTextEditor
if (editor && !editor.selection.isEmpty) {
const selection = editor.selection
const selectedText = editor.document.getText(selection)
const filePath = editor.document.uri.fsPath

// Convert to workspace-relative path if possible
const workspacePath = provider.cwd
let relativeFilePath: string
if (filePath.startsWith(workspacePath)) {
relativeFilePath = path.relative(workspacePath, filePath)
} else {
// File is outside workspace, use absolute path
relativeFilePath = filePath
}

// VSCode uses 0-based line numbers, convert to 1-based for user-friendly display
const startLine = selection.start.line + 1
const endLine = selection.end.line + 1

// Send selection context to webview only - don't store in task
await provider.postMessageToWebview({
type: "selectionContext",
selectedText,
selectionFilePath: relativeFilePath,
selectionStartLine: startLine,
selectionEndLine: endLine,
})
} else {
// No selection, send empty context
await provider.postMessageToWebview({
type: "selectionContext",
})
}
break
}
case "webviewDidLaunch":
// Load custom modes first
const customModes = await provider.customModesManager.getCustomModes()
Expand Down Expand Up @@ -507,7 +545,21 @@ export const webviewMessageHandler = async (
// agentically running promises in old instance don't affect our new
// task. This essentially creates a fresh slate for the new task.
try {
await provider.createTask(message.text, message.images)
await provider.createTask(message.text, message.images, undefined, {
selectionContext:
message.selectedText &&
message.selectionFilePath &&
typeof message.selectionStartLine === "number" &&
typeof message.selectionEndLine === "number"
? {
selectedText: message.selectedText,
selectionFilePath: message.selectionFilePath,
selectionStartLine: message.selectionStartLine,
selectionEndLine: message.selectionEndLine,
}
: undefined,
})

// Task created successfully - notify the UI to reset
await provider.postMessageToWebview({ type: "invoke", invoke: "newChat" })
} catch (error) {
Expand All @@ -523,9 +575,24 @@ export const webviewMessageHandler = async (
await provider.updateCustomInstructions(message.text)
break

case "askResponse":
provider.getCurrentTask()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
case "askResponse": {
const task = provider.getCurrentTask()
// Pass selection context through to handleWebviewAskResponse
const selectionContext =
message.selectedText &&
message.selectionFilePath &&
typeof message.selectionStartLine === "number" &&
typeof message.selectionEndLine === "number"
? {
selectedText: message.selectedText,
selectionFilePath: message.selectionFilePath,
selectionStartLine: message.selectionStartLine,
selectionEndLine: message.selectionEndLine,
}
: undefined
task?.handleWebviewAskResponse(message.askResponse!, message.text, message.images, selectionContext)
break
}

case "updateSettings":
if (message.updatedSettings) {
Expand Down
5 changes: 5 additions & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,12 @@ export interface ExtensionMessage {
| "dismissedUpsells"
| "organizationSwitchResult"
| "interactionRequired"
| "selectionContext"
text?: string
selectedText?: string
selectionFilePath?: string
selectionStartLine?: number
selectionEndLine?: number
payload?: any // Add a generic payload for now, can refine later
// Checkpoint warning message
checkpointWarning?: {
Expand Down
5 changes: 5 additions & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,12 @@ export interface WebviewMessage {
| "dismissUpsell"
| "getDismissedUpsells"
| "updateSettings"
| "requestSelectionContext"
text?: string
selectedText?: string
selectionFilePath?: string
selectionStartLine?: number
selectionEndLine?: number
editedMessageContent?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
disabled?: boolean
Expand Down
Loading