Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Async terminal for CLI in chat panel#312178

Draft
anthonykim1 wants to merge 18 commits intomainfrom
anthonykim1/supportAsyncTerminalInCopilotCLIchatPanel
Draft

Async terminal for CLI in chat panel#312178
anthonykim1 wants to merge 18 commits intomainfrom
anthonykim1/supportAsyncTerminalInCopilotCLIchatPanel

Conversation

@anthonykim1
Copy link
Copy Markdown
Contributor

@anthonykim1 anthonykim1 commented Apr 23, 2026

Resolves: #309290

  • Still very much in progress.
  • Please see comments below (videos, screenshot, comments)
  • Feel free to fetch this branch locally and play around.

Both route with/without controller api should work.

@anthonykim1 anthonykim1 self-assigned this Apr 23, 2026
Copilot AI review requested due to automatic review settings April 23, 2026 17:13
Copy link
Copy Markdown
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 introduces a proposed chat sessions API for extensions to inject system-initiated “notification-style” turns into chat sessions they own, and wires it up for Copilot CLI sessions so async SDK/terminal completions can surface as new chat bubbles.

Changes:

  • Add vscode.chat.sendSystemInitiatedRequest(...) proposed API + options type.
  • Implement ext host ↔ main thread plumbing to enqueue a steering, system-initiated chat request with a label.
  • Update Copilot CLI chat sessions to forward SDK system.notification events into the chat panel and keep SDK sessions alive long enough for async completions to arrive.
Show a summary per file
File Description
src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts Adds proposed API surface for system-initiated chat requests.
src/vs/workbench/api/common/extHostChatSessions.ts Validates ownership + forwards requests to main thread via RPC.
src/vs/workbench/api/common/extHost.protocol.ts Extends main-thread chat sessions shape with $sendSystemInitiatedRequest.
src/vs/workbench/api/common/extHost.api.impl.ts Exposes chat.sendSystemInitiatedRequest to extensions under proposed API gate.
src/vs/workbench/api/browser/mainThreadChatSessions.ts Receives RPC and enqueues a steering system-initiated request in IChatService.
extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessions.ts Forwards CLI SDK system notifications into chat via the new API; passes through isSystemInitiated.
extensions/copilot/src/extension/chatSessions/copilotcli/node/test/testHelpers.ts Updates mock SDK session helper with an on(...) method to match usage.
extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotcliSession.spec.ts Extends test mock with getBackgroundTasks() to satisfy new logging/inspection.
extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts Emits onDidReceiveSystemNotification and adds a post-turn keep-alive ref window.
extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts Translates SDK system.notification to a typed event; adds system-initiated request handling path.

Copilot's findings

Comments suppressed due to low confidence (1)

extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts:357

  • There is new behavior for request.isSystemInitiated that changes how requests are handled (skipping _sdkSession.send() and waiting for the next assistant.turn_end). The existing unit tests for CopilotCLISession don't cover the isSystemInitiated path; please add a test that asserts the SDK send() is not called and that the handler resolves on assistant.turn_end / cancellation.
			}

			if (isAlreadyBusyWithAnotherRequest) {
				return this._handleRequestSteering(input, attachments, model, previousRequestSnapshot, token);
			} else {
				return this._handleRequestImpl(request, input, attachments, model, token, /*isSystemInitiated*/ false);
			}
		});
  • Files reviewed: 9/10 changed files
  • Comments generated: 5

Comment thread src/vs/workbench/api/browser/mainThreadChatSessions.ts
Comment thread src/vs/workbench/api/browser/mainThreadChatSessions.ts Outdated
@anthonykim1
Copy link
Copy Markdown
Contributor Author

anthonykim1 commented Apr 23, 2026

@DonJayamanne @rebornix @mattbierner

Don't think activeResponseCallback in

/**
* Callback invoked by the editor for a currently running response. This allows the session to push items for the
* current response and stream these in as them come in. The current response will be considered complete once the
* callback resolved.
*
* If not provided, the chat session is assumed to not currently be running.
*/
readonly activeResponseCallback?: (stream: ChatResponseStream, token: CancellationToken) => Thenable<void>;
would do it since it would show the progress shimmer that the response is still in progress?

I think the biggest thing against activateResponseCallback is that ui would be stuck in perpetual shimmer/responding state with all notification glued onto the previous assistant turn as if the model never stopped talking, and user wouldn't be able to send follow ups without killing the very thing that delivers the notification.

VS.

(This PR with new proposed api):

The approach from the PR we'd have no stalling shimmer, but would have new compact "system" bubble appear for async output such as sleep 5 finished

  • Also user can type follow up anytime (input not blocked).
  • Multiple async shell should work via multiple discrete bubble (clearly its own event).

@anthonykim1
Copy link
Copy Markdown
Contributor Author

anthonykim1 commented Apr 23, 2026

@roblourens @TylerLeonhardt
Here is what it would look like as of Apr 23rd, 2:23 p.m. PST:

Screen.Recording.2026-04-23.at.2.22.09.PM.mov

Here is equivalent in Copilot CLI.
cliAsyncExample

We could improve to better distinguish the async terminal output but it seems like CLI themselves are not doing explicit labeling either. Maybe the color of the decorations are meant to signify something?

/cc @justschen For chat ui

// task list only includes *detached* shells and background agents, not
// plain `mode: "async"` shells (they're tracked internally by
// `shellContext.currentExecutions`). A status-based window matches the
// existing `sessionTerminators` 5-minute lifetime and covers every
Copy link
Copy Markdown
Contributor Author

@anthonykim1 anthonykim1 Apr 23, 2026

Choose a reason for hiding this comment

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

@DonJayamanne @roblourens I'm not so sure about this 5-minute lifetime thing. I can see it was existing previously but don't have context over what specific thing it was meant for, maybe we can do something better here for async terminals and keeping session alive.

@github-actions
Copy link
Copy Markdown
Contributor

Screenshot Changes

Base: 5f818f4c Current: 42506f1e

Changed (1)

sessions/sessionsPolicyBlocked/Loading/Light
Before After
before after

this.sessionId = _sdkSession.sessionId;
this.add(toDisposable(() => this._todoSqlQuery.dispose()));

// Forward SDK system notifications (async/detached shell completions,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll probably delete this before final review but thought it would be helpful for earlier people who read, understand more carefully.

const disposables = new DisposableStore();
try {
await new Promise<void>(resolve => {
const off = this._sdkSession.on('assistant.turn_end', () => resolve());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what happens if the next turn also starts an async terminal call?

try {
const notification = this._buildSystemNotification(event);
if (notification) {
this._onDidReceiveSystemNotification.fire(notification);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What happens if the async terminal completes within the current turn?
I think with this approach we will always end up going out of there and sending a message to core and then re-enter as another request handler when it could've been handled in a single turn.

session.add(toDisposable(releaseKeepAlive));
session.add(session.onDidChangeStatus(() => {
const status = session.status;
if (session.permissionRequested) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please do not use permissionRequested, this will be removed.

keepAliveTimer.clear();
return;
}
if (status === undefined || status === ChatSessionStatus.Completed || status === ChatSessionStatus.Failed) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Doesn't this mean that ALL sessions will always keep a ref to this object and never release it?

* Rendered as a compact system-notification chip in the chat transcript
* (e.g. ``"`sleep 10` completed"``).
*/
readonly systemInitiatedLabel: string;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

perhaps prompt is a better property
If its a request thats being sent, then i would like to think the message thats sent as part of the request would be a prompt or command, & this case it should be a prompt.

@DonJayamanne DonJayamanne requested a review from meganrogge April 23, 2026 23:41
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.

Async terminals don't resume the session

3 participants