Skip to content

Comments

Function Host Debugger View#4883

Open
nturinski wants to merge 44 commits intomainfrom
nat/debuggerView
Open

Function Host Debugger View#4883
nturinski wants to merge 44 commits intomainfrom
nat/debuggerView

Conversation

@nturinski
Copy link
Member

This PR adds a Function Host Debug view to the Azure Functions extension. The view appears in the Run and Debug panel and lists running Function Host tasks along with their recent error output.

A new tree view provider backs this view and supplies nodes for active hosts and error entries. The view includes commands to refresh the data, clear recorded errors, view or copy recent logs, and send an error to Copilot. These commands are available from the view toolbar and node context menus.

The PR also introduces a setting to always show the Function Host Debug view, regardless of context.

Supporting changes include exporting shared utilities for identifying host error logs and cleaning log output, extending command handling to accept argument arrays, and updating the Functions task provider to allow additional CLI arguments in task definitions.

No host running (only visible if always show setting is enabled)
image

Function host running with no errors
image

Function host running with an error
image

Location of Ask Copilot context menu
image

nturinski and others added 30 commits October 24, 2025 10:55
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a Function Host Debug view to the Azure Functions extension, providing developers with visibility into running Function Host tasks and their error output directly in the Run and Debug panel.

Changes:

  • Adds a new tree view in the Debug panel showing running Function Host tasks with their errors
  • Implements ANSI stripping utilities and error log detection based on terminal color codes
  • Integrates GitHub Copilot Chat for AI-assisted error diagnosis
  • Adds commands for viewing/copying logs and clearing errors
  • Extends task provider to support additional CLI arguments in task definitions

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
src/debug/FunctionHostDebugView.ts Tree data provider for the debug view, handles node creation and hierarchy
src/debug/registerFunctionHostDebugView.ts Registers view commands (refresh, clear errors, show logs, ask Copilot) and handlers
src/funcCoreTools/funcHostTask.ts Tracks running tasks, captures terminal output streams, detects and stores error logs
src/funcCoreTools/funcHostErrorUtils.ts Utilities for identifying error logs via ANSI codes and extracting error context
src/utils/ansiUtils.ts ANSI escape sequence removal for clean text presentation
src/utils/copilotChat.ts Best-effort integration to open Copilot Chat with pre-filled prompts
src/debug/FuncTaskProvider.ts Extends task definitions to support additional command-line arguments
src/tree/localProject/LocalProjectTreeItem.ts Updates event handlers to use new event object pattern
src/commands/pickFuncProcess.ts Returns terminal stream in API response for external consumers
src/extension.ts Registers the new debug view and disposes terminal event reader on deactivation
package.json Defines view contribution, commands, menus, and new setting
package.nls.json Adds localized strings for the new feature
extension.bundle.ts Exports new utility functions for external use
test/funcHostErrorContext.test.ts Tests for error detection and context extraction
test/ansiUtils.test.ts Tests for ANSI control character stripping

Comment on lines +168 to +173
terminalEventReader = vscode.window.onDidStartTerminalShellExecution(async (terminalShellExecEvent) => {
/**
* This will pick up any terminal that, including those started outside of tasks (e.g. via the command palette).
* But we don't actually access the terminal stream until the `func host start` task starts, at which time this will be pointing to the correct terminal
* */
latestTerminalShellExecutionEvent = terminalShellExecEvent;
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

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

The terminal shell execution event listener is registered before func host task starts and stores the latest event in a global variable. This creates a race condition: if multiple terminals are opened or if a non-func terminal is opened just before the func task starts, latestTerminalShellExecutionEvent could point to the wrong terminal. Consider matching terminals more precisely by correlating the task execution with the terminal, or by checking if the terminal's command matches the func host start pattern when consuming the stream.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

The main issue here is that onDidStartTerminalShellExecution always fires before onDidStartTask so if I register within onDidStartTask, it'll be too late.

The other issue is that for whatever reason, the TerminalShellExecution doesn't properly display custom tasks so I can't even determine if it's a function task by commandLine. If I look at the terminal, since it fires before the terminal UI actually updates, the terminal name is incorrect.

Copy link
Member Author

Choose a reason for hiding this comment

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

@copilot open a new pull request to apply changes based on this feedback

Copy link
Member Author

Choose a reason for hiding this comment

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

Curious to see what it comes up with.

Comment on lines +87 to +178
export function registerFunctionHostDebugView(context: vscode.ExtensionContext): void {
const provider = new FuncHostDebugViewProvider();

context.subscriptions.push(
vscode.window.registerTreeDataProvider(viewId, provider),
onRunningFuncTasksChanged(() => {
provider.refresh();
void tryOpenDebugViewOnFirstFuncHostError();
}),
);

// Ensure the context key is correct on activation.
void refreshFuncHostDebugContext();

registerCommand('azureFunctions.funcHostDebug.clearErrors', async (actionContext: IActionContext) => {
actionContext.telemetry.properties.source = 'funcHostDebugView';
for (const folder of vscode.workspace.workspaceFolders ?? []) {
for (const t of runningFuncTaskMap.getAll(folder)) {
if (!t) {
continue;
}

if ((t.errorLogs?.length ?? 0) > 0) {
t.errorLogs = [];
}
}
}

provider.refresh();
});

registerCommand('azureFunctions.funcHostDebug.copyRecentLogs', async (actionContext: IActionContext, args: unknown) => {
actionContext.telemetry.properties.source = 'funcHostDebugView';
if (!isHostTaskNode(args)) {
return;
}

const task = runningFuncTaskMap.get(args.workspaceFolder, args.cwd);
const text = getRecentLogsPlainText(task);
await vscode.env.clipboard.writeText(text);
});

registerCommand('azureFunctions.funcHostDebug.showRecentLogs', async (actionContext: IActionContext, args: unknown) => {
actionContext.telemetry.properties.source = 'funcHostDebugView';
if (!isHostTaskNode(args)) {
return;
}

const task = runningFuncTaskMap.get(args.workspaceFolder, args.cwd);
const text = getRecentLogsPlainText(task);

const doc = await vscode.workspace.openTextDocument({
content: text || localize('funcHostDebug.noLogs', 'No logs captured yet.'),
language: 'log',
});
await vscode.window.showTextDocument(doc, { preview: true });
});

registerCommand('azureFunctions.funcHostDebug.askCopilot', async (actionContext: IActionContext, args: unknown) => {
actionContext.telemetry.properties.source = 'funcHostDebugView';
if (!isHostErrorNode(args)) {
return;
}

// Use the exact message shown in the error node tooltip.
const errorContext = stripAnsiControlCharacters(args.message).trim() || args.message;

const scopeLabel = typeof args.workspaceFolder === 'object'
? args.workspaceFolder.name
: localize('funcHostDebug.globalScope', 'Global');

const prompt = [
'I am debugging an Azure Functions project locally in VS Code.',
`Function Host Port: ${args.portNumber}`,
`Workspace: ${scopeLabel}`,
args.cwd ? `CWD: ${args.cwd}` : undefined,
'',
'The Functions host produced an error. Diagnose the likely cause and suggest concrete next steps to fix it.',
'',
'Error output:',
errorContext,
].filter((l): l is string => Boolean(l)).join('\n');

await openCopilotChat(prompt);
});

registerCommand('azureFunctions.funcHostDebug.refresh', async (actionContext: IActionContext) => {
actionContext.telemetry.properties.source = 'funcHostDebugView';
provider.refresh();
await refreshFuncHostDebugContext();
});
}
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

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

The new FuncHostDebugViewProvider class and registerFunctionHostDebugView function lack test coverage. Given that other components in the repository have comprehensive test coverage (as seen in the test directory), these new components should also have tests covering tree item creation, command handlers, error node generation, and the refresh logic.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

@copilot open a new pull request to apply changes based on this feedback

Comment on lines +14 to +44
export async function openCopilotChat(prompt: string): Promise<void> {
const trimmed = (prompt ?? '').trim();
if (!trimmed) {
return;
}

const candidates: Array<{ command: string; args: unknown[] }> = [
// Newer VS Code variants
{ command: 'workbench.action.chat.open', args: [trimmed] },
{ command: 'workbench.action.chat.open', args: [{ query: trimmed }] },
// Older / alternate variants
{ command: 'workbench.action.openChat', args: [trimmed] },
// Copilot extensions (IDs vary by version)
{ command: 'github.copilot.openChat', args: [trimmed] },
{ command: 'github.copilot-chat.openChat', args: [trimmed] },
];

for (const { command, args } of candidates) {
try {
await vscode.commands.executeCommand(command, ...args);
return;
} catch {
// Ignore and try the next candidate
}
}

void vscode.window.showWarningMessage(localize(
'funcHostDebug.copilotChatUnavailable',
'Unable to open Copilot Chat. Please ensure GitHub Copilot Chat is installed and enabled.'
));
}
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

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

The openCopilotChat function lacks test coverage. This function attempts multiple command variants and should be tested to verify it tries all fallbacks and shows the appropriate warning message when all attempts fail.

Copilot uses AI. Check for mistakes.
const redAnsiRegex = /\u001b\[(?:[0-9;]*31m|[0-9;]*91m|38;5;(9|1)m)/;

export function isFuncHostErrorLog(log: string): boolean {
return redAnsiRegex.test(log);
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

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

The isFuncHostErrorLog function only checks for red ANSI codes, but error logs might not always be colored red (depending on the terminal configuration or the Functions runtime version). Consider adding fallback detection for common error patterns like lines starting with "Error:", "[Error]", "Exception:", etc., to make error detection more robust across different environments.

Suggested change
return redAnsiRegex.test(log);
// First, prefer detection based on red ANSI color codes (backwards compatible behavior).
if (redAnsiRegex.test(log)) {
return true;
}
// Fallback: detect common error patterns in the plain-text log line (no ANSI/control chars).
const plain = stripAnsiControlCharacters(log).trim();
if (!plain) {
return false;
}
const lower = plain.toLowerCase();
// Typical error prefixes from Functions host logs and similar environments.
if (lower.startsWith('error:') || lower.startsWith('[error]') || lower.startsWith('exception:')) {
return true;
}
return false;

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI commented Jan 15, 2026

@nturinski I've opened a new pull request, #4885, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Contributor

Copilot AI commented Jan 15, 2026

@nturinski I've opened a new pull request, #4886, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Contributor

Copilot AI commented Jan 15, 2026

@nturinski I've opened a new pull request, #4887, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI and others added 2 commits January 14, 2026 16:15
…4885)

* Initial plan

* Add JSDoc documentation for IStartFuncProcessResult interface

Co-authored-by: nturinski <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: nturinski <[email protected]>
* Initial plan

* Add AbortController to prevent infinite stream iteration

Co-authored-by: nturinski <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: nturinski <[email protected]>
Copy link
Contributor

Copilot AI commented Jan 15, 2026

@nturinski I've opened a new pull request, #4888, to work on those changes. Once the pull request is ready, I'll request review from you.

nturinski and others added 2 commits January 14, 2026 16:35
Removed exhaustive check for FuncHostDebugNode.
const funcCliPath = await getFuncCliPath(context, folder);
const args = (definition?.args || []) as string[];
if (args.length > 0) {
command = `${command} ${args.join(' ')}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

Since the VSCode Task API accepts argument lists, why not use that?

Comment on lines +20 to +29
const candidates: Array<{ command: string; args: unknown[] }> = [
// Newer VS Code variants
{ command: 'workbench.action.chat.open', args: [trimmed] },
{ command: 'workbench.action.chat.open', args: [{ query: trimmed }] },
// Older / alternate variants
{ command: 'workbench.action.openChat', args: [trimmed] },
// Copilot extensions (IDs vary by version)
{ command: 'github.copilot.openChat', args: [trimmed] },
{ command: 'github.copilot-chat.openChat', args: [trimmed] },
];
Copy link
Contributor

Choose a reason for hiding this comment

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

Does the minimum VSCode version we have in this extension exclude any of these?

@MicroFish91
Copy link
Contributor

Very cool, I have it as a todo to review this either tomorrow or early next week!

return tasks;
}

async function updateFuncHostDebugContext(): Promise<void> {
Copy link
Contributor

@MicroFish91 MicroFish91 Jan 28, 2026

Choose a reason for hiding this comment

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

I'm running into an issue on error where the Function Host Debug view is not showing up. Well, at least that's what it appeared like initially. In reality, after I set some breakpoints, I realized that it actually is getting shown, but then immediately gets hidden.

I think for some reason this update function is called to show the view, and then for some reason gets called again and causes the view to hide.

My repro steps were: (1) create a DTS project via createNewProject, (2) hit F5, and (3) answer skip for now so nothing gets set for both connections. This causes this func CLI error which I was expecting to show up:

Image

I should be in office tomorrow so if you're not able to repro maybe we can investigate on my machine, lmk!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants