Skip to content

Commit 1b256d7

Browse files
committed
plan with local agent when using @copilot
1 parent c511e34 commit 1b256d7

3 files changed

Lines changed: 247 additions & 73 deletions

File tree

src/github/dashboardWebviewProvider.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,9 @@ export class DashboardWebviewProvider extends WebviewBase {
423423
case 'submit-chat':
424424
await this.handleChatSubmission(message.args?.query);
425425
break;
426+
case 'plan-task-with-local-agent':
427+
await this.handlePlanTaskWithLocalAgent(message.args?.query);
428+
break;
426429
case 'open-session':
427430
await this.openSession(message.args?.sessionId);
428431
break;
@@ -942,6 +945,68 @@ export class DashboardWebviewProvider extends WebviewBase {
942945
}
943946
}
944947

948+
/**
949+
* Handles planning a task with local agent - opens issue side-by-side with chat
950+
*/
951+
private async handlePlanTaskWithLocalAgent(query: string): Promise<void> {
952+
if (!query) {
953+
return;
954+
}
955+
956+
try {
957+
// Extract issue references from the query to find related issues
958+
const issueReferences = this.extractIssueReferences(query);
959+
960+
if (issueReferences.length > 0) {
961+
// If there are issue references, try to open the first one side-by-side with chat
962+
const firstIssue = issueReferences[0];
963+
964+
// Find the issue model for the referenced issue
965+
for (const folderManager of this._repositoriesManager.folderManagers) {
966+
try {
967+
const issueModel = await folderManager.resolveIssue(
968+
firstIssue.owner || folderManager.gitHubRepositories[0]?.remote.owner || '',
969+
firstIssue.repo || folderManager.gitHubRepositories[0]?.remote.repositoryName || '',
970+
firstIssue.number
971+
);
972+
if (issueModel) {
973+
// Use the existing side-by-side command
974+
await vscode.commands.executeCommand('issue.openIssueAndCodingAgentSideBySide', issueModel);
975+
return;
976+
}
977+
} catch (error) {
978+
// Continue to try other folder managers
979+
continue;
980+
}
981+
}
982+
}
983+
984+
// If no specific issue found, create a general planning session
985+
// Open a new chat session with the query and planning instructions
986+
await vscode.commands.executeCommand('workbench.action.chat.newChat');
987+
988+
const planningQuery = `I want to plan and analyze this task before implementing it:
989+
990+
${query}
991+
992+
Please help me:
993+
1. Break down what needs to be implemented
994+
2. Identify any potential challenges or considerations
995+
3. Suggest an implementation approach
996+
4. Ask any clarifying questions that would help create better instructions for a coding agent
997+
998+
Keep your response focused and actionable - ask at most 3 essential questions if there are genuine ambiguities.`;
999+
1000+
await vscode.commands.executeCommand('workbench.action.chat.open', {
1001+
query: planningQuery
1002+
});
1003+
1004+
} catch (error) {
1005+
Logger.error(`Failed to plan task with local agent: ${error}`, DashboardWebviewProvider.ID);
1006+
vscode.window.showErrorMessage('Failed to open planning session. Make sure the Chat extension is available.');
1007+
}
1008+
}
1009+
9451010
/**
9461011
* Determines if a query represents a coding task vs a general question using VS Code's Language Model API
9471012
*/

webviews/dashboardView/components/ChatInput.tsx

Lines changed: 105 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -86,19 +86,20 @@ function setupMonaco() {
8686
// Check if user is typing after #
8787
const hashMatch = textUntilPosition.match(/#\d*$/);
8888
if (hashMatch) {
89-
const suggestions = suggestionDataSource?.state === 'ready' ? suggestionDataSource.milestoneIssues.map((issue): monaco.languages.CompletionItem => ({
90-
label: `#${issue.number}`,
91-
kind: monaco.languages.CompletionItemKind.Reference,
92-
insertText: `#${issue.number}`,
93-
detail: issue.title,
94-
documentation: `Issue #${issue.number}: ${issue.title}\nAssignee: ${issue.assignee || 'None'}\nMilestone: ${issue.milestone || 'None'}`,
95-
range: {
96-
startLineNumber: position.lineNumber,
97-
startColumn: position.column - hashMatch[0].length,
98-
endLineNumber: position.lineNumber,
99-
endColumn: position.column
100-
}
101-
})) : [];
89+
const suggestions = (suggestionDataSource?.state === 'ready' && !suggestionDataSource.isGlobal)
90+
? suggestionDataSource.milestoneIssues.map((issue: any): monaco.languages.CompletionItem => ({
91+
label: `#${issue.number}`,
92+
kind: monaco.languages.CompletionItemKind.Reference,
93+
insertText: `#${issue.number}`,
94+
detail: issue.title,
95+
documentation: `Issue #${issue.number}: ${issue.title}\nAssignee: ${issue.assignee || 'None'}\nMilestone: ${issue.milestone || 'None'}`,
96+
range: {
97+
startLineNumber: position.lineNumber,
98+
startColumn: position.column - hashMatch[0].length,
99+
endLineNumber: position.lineNumber,
100+
endColumn: position.column
101+
}
102+
})) : [];
102103

103104
return { suggestions };
104105
}
@@ -147,7 +148,7 @@ interface ChatInputProps {
147148

148149
export const ChatInput: React.FC<ChatInputProps> = ({ data, isGlobal }) => {
149150
const [chatInput, setChatInput] = useState('');
150-
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor | null>(null);
151+
const [showDropdown, setShowDropdown] = useState(false);
151152

152153
// Handle content changes
153154
const handleEditorChange = useCallback((value: string | undefined) => {
@@ -168,39 +169,43 @@ export const ChatInput: React.FC<ChatInputProps> = ({ data, isGlobal }) => {
168169
}
169170
}, [chatInput]);
170171

171-
// Handle quick action button clicks with input submission
172-
const handleQuickAction = useCallback((prefix: string) => {
173-
// If there's existing input, prepend the prefix and submit
174-
const finalInput = chatInput.trim() ? `${prefix}${chatInput.trim()}` : '';
175172

176-
if (finalInput.trim()) {
177-
// Send the combined input
173+
174+
// Handle dropdown option for planning task with local agent
175+
const handlePlanWithLocalAgent = useCallback(() => {
176+
if (chatInput.trim()) {
177+
const trimmedInput = chatInput.trim();
178+
// Remove @copilot prefix for planning with local agent
179+
const cleanQuery = trimmedInput.replace(/@copilot\s*/, '').trim();
180+
181+
// Send command to plan task with local agent
178182
vscode.postMessage({
179-
command: 'submit-chat',
180-
args: { query: finalInput }
183+
command: 'plan-task-with-local-agent',
184+
args: { query: cleanQuery }
181185
});
182186

183-
// Clear the input
184187
setChatInput('');
185-
} else {
186-
// If no input, just set the prefix and focus editor
187-
setChatInput(prefix);
188-
if (editor) {
189-
editor.focus();
190-
// Position cursor at the end
191-
const model = editor.getModel();
192-
if (model) {
193-
const position = model.getPositionAt(prefix.length);
194-
editor.setPosition(position);
195-
}
188+
setShowDropdown(false);
189+
}
190+
}, [chatInput]);
191+
192+
// Handle clicking outside dropdown to close it
193+
useEffect(() => {
194+
const handleClickOutside = (event: Event) => {
195+
const target = event.target as HTMLElement;
196+
if (!target.closest('.send-button-container')) {
197+
setShowDropdown(false);
196198
}
199+
};
200+
201+
if (showDropdown) {
202+
document.addEventListener('click', handleClickOutside);
203+
return () => document.removeEventListener('click', handleClickOutside);
197204
}
198-
}, [chatInput, editor]);
205+
}, [showDropdown]);
199206

200207
// Setup editor instance when it mounts
201208
const handleEditorDidMount = useCallback((editorInstance: monaco.editor.IStandaloneCodeEditor, monaco: Monaco) => {
202-
setEditor(editorInstance);
203-
204209
// Auto-resize editor based on content
205210
const updateHeight = () => {
206211
const model = editorInstance.getModel();
@@ -284,53 +289,80 @@ export const ChatInput: React.FC<ChatInputProps> = ({ data, isGlobal }) => {
284289
automaticLayout: true
285290
}}
286291
/>
287-
<button
288-
className="send-button-inline"
289-
onClick={handleSendChat}
290-
disabled={!chatInput.trim()}
291-
title={
292-
isCopilotCommand(chatInput)
293-
? 'Start new remote Copilot task (Ctrl+Enter)'
294-
: isLocalCommand(chatInput)
292+
{isCopilotCommand(chatInput) ? (
293+
<div className="send-button-container">
294+
<button
295+
className="send-button-inline split-left"
296+
onClick={handleSendChat}
297+
disabled={!chatInput.trim()}
298+
title="Start new remote Copilot task (Ctrl+Enter)"
299+
>
300+
<span style={{ marginRight: '4px', fontSize: '12px' }}>Start remote task</span>
301+
<span className="codicon codicon-send"></span>
302+
</button>
303+
<button
304+
className="send-button-inline split-right"
305+
onClick={(e) => {
306+
e.stopPropagation();
307+
setShowDropdown(!showDropdown);
308+
}}
309+
disabled={!chatInput.trim()}
310+
title="More options"
311+
>
312+
<span className="codicon codicon-chevron-down"></span>
313+
</button>
314+
{showDropdown && (
315+
<div className="dropdown-menu">
316+
<button
317+
className="dropdown-item"
318+
onClick={handlePlanWithLocalAgent}
319+
>
320+
<span>Plan task with local agent</span>
321+
<span className="codicon codicon-comment-discussion" style={{ marginLeft: '8px' }}></span>
322+
</button>
323+
</div>
324+
)}
325+
</div>
326+
) : (
327+
<button
328+
className="send-button-inline"
329+
onClick={handleSendChat}
330+
disabled={!chatInput.trim()}
331+
title={
332+
isLocalCommand(chatInput)
295333
? 'Start new local task (Ctrl+Enter)'
296334
: 'Send message (Ctrl+Enter)'
297-
}
298-
>
299-
<span style={{ marginRight: '4px', fontSize: '12px' }}>
300-
{isCopilotCommand(chatInput)
301-
? 'Start remote task'
302-
: isLocalCommand(chatInput)
335+
}
336+
>
337+
<span style={{ marginRight: '4px', fontSize: '12px' }}>
338+
{isLocalCommand(chatInput)
303339
? 'Start local task'
304340
: 'Send'
305-
}
306-
</span>
307-
<span className="codicon codicon-send"></span>
308-
</button>
341+
}
342+
</span>
343+
<span className="codicon codicon-send"></span>
344+
</button>
345+
)}
309346
</div>
310347
</div>
311348

312349
{isGlobal && <GlobalInstructions />}
313350

314351
<div className="quick-actions">
315352
{!isGlobal && (
316-
<>
317-
<div
318-
className="quick-action-button"
319-
onClick={() => handleQuickAction('@copilot ')}
320-
title="Start remote GitHub agent - works in background and creates a PR"
321-
>
322-
<span className="codicon codicon-robot"></span>
323-
<span>Start background task on GitHub</span>
324-
</div>
325-
<div
326-
className="quick-action-button"
327-
onClick={() => handleQuickAction('@local ')}
328-
title="Create new branch and work locally with chat assistance"
329-
>
330-
<span className="codicon codicon-device-desktop"></span>
331-
<span>Start local task</span>
353+
<div className="global-instructions">
354+
<div className="instructions-content">
355+
<p>
356+
<strong>Reference issues:</strong> Use the syntax <code>org/repo#123</code> to start work on specific issues from any repository.
357+
</p>
358+
<p>
359+
<strong>Choose your agent:</strong> Use <code>@local</code> to work locally or <code>@copilot</code> to use GitHub Copilot.
360+
</p>
361+
<p>
362+
<strong>Mention projects:</strong> You can talk about projects by name to work across multiple repositories.
363+
</p>
332364
</div>
333-
</>
365+
</div>
334366
)}
335367

336368
{/* Removed QuickActions for global dashboards - moved to input area separator only */}
@@ -348,4 +380,4 @@ const isLocalCommand = (text: string): boolean => {
348380
return text.trim().startsWith('@local');
349381
};
350382

351-
setupMonaco();
383+
setupMonaco();

webviews/dashboardView/index.css

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,83 @@ body {
581581
opacity: 0.6;
582582
}
583583

584+
/* Send button container for split button */
585+
.send-button-container {
586+
position: absolute;
587+
right: 14px;
588+
bottom: 6px;
589+
display: flex;
590+
height: 28px;
591+
}
592+
593+
.monaco-input-wrapper .send-button-inline.split-left {
594+
border-radius: 3px 0 0 3px !important;
595+
border-right: 1px solid var(--vscode-button-separator) !important;
596+
position: relative;
597+
right: auto;
598+
bottom: auto;
599+
height: 100%;
600+
}
601+
602+
.monaco-input-wrapper .send-button-inline.split-right {
603+
border-radius: 0 3px 3px 0 !important;
604+
width: 24px !important;
605+
min-width: 24px !important;
606+
padding: 0 4px !important;
607+
position: relative;
608+
right: auto;
609+
bottom: auto;
610+
height: 100%;
611+
}
612+
613+
/* Dropdown menu */
614+
.dropdown-menu {
615+
position: absolute;
616+
top: 100%;
617+
right: 0;
618+
margin-top: 4px;
619+
background-color: var(--vscode-dropdown-background);
620+
border: 1px solid var(--vscode-dropdown-border);
621+
border-radius: 4px;
622+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
623+
z-index: 1000;
624+
min-width: 200px;
625+
}
626+
627+
.dropdown-item {
628+
display: flex !important;
629+
align-items: center !important;
630+
width: 100% !important;
631+
padding: 8px 12px !important;
632+
border: none !important;
633+
background: transparent !important;
634+
color: var(--vscode-dropdown-foreground) !important;
635+
cursor: pointer !important;
636+
font-size: 13px !important;
637+
text-align: left !important;
638+
border-radius: 0 !important;
639+
box-shadow: none !important;
640+
outline: none !important;
641+
white-space: nowrap !important;
642+
}
643+
644+
.dropdown-item:hover {
645+
background-color: var(--vscode-list-hoverBackground) !important;
646+
}
647+
648+
.dropdown-item:first-child {
649+
border-radius: 3px 3px 0 0 !important;
650+
}
651+
652+
.dropdown-item:last-child {
653+
border-radius: 0 0 3px 3px !important;
654+
}
655+
656+
.dropdown-item .codicon {
657+
margin-right: 8px !important;
658+
font-size: 14px !important;
659+
}
660+
584661
.empty-state {
585662
text-align: center;
586663
color: var(--vscode-descriptionForeground);

0 commit comments

Comments
 (0)