Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4f76933
Add standard ctrl/cmd + C or + x commands for vscode shell
GeorgeNgMsft Jul 21, 2026
f9c3a7f
Make chat input drag-selectable from the box padding
GeorgeNgMsft Jul 21, 2026
e1016bc
Improve reasoning action UI (Thinking blocks, working rail, spacing)
GeorgeNgMsft Jul 21, 2026
468b439
Reasoning UI: list/heading spacing, bigger answer gap, expand-message…
GeorgeNgMsft Jul 21, 2026
f22a17d
Expand message: open in a movable VS Code editor panel
GeorgeNgMsft Jul 21, 2026
a8c261b
Make webview panel tab icon visible on dark themes
GeorgeNgMsft Jul 21, 2026
4d8a2db
docs: regenerate README.AUTOGEN.md, command reference, and action bro…
typeagent-bot[bot] Jul 21, 2026
1f5cea4
docs: regenerate README.AUTOGEN.md, command reference, and action bro…
typeagent-bot[bot] Jul 21, 2026
ccf9841
Clean up reasoning/expand PR: DRY helpers, tighter comments
GeorgeNgMsft Jul 21, 2026
323ff62
Merge branch 'dev/georgeng/reasoning_action_ui' of https://github.com…
GeorgeNgMsft Jul 21, 2026
17ffbd7
Merge branch 'main' into dev/georgeng/reasoning_action_ui
GeorgeNgMsft Jul 21, 2026
bf2bc7f
Merge branch 'main' into dev/georgeng/vscode_copy
GeorgeNgMsft Jul 21, 2026
aaa085c
Merge branch 'dev/georgeng/vscode_copy' of https://github.com/microso…
GeorgeNgMsft Jul 21, 2026
313585d
docs: regenerate README.AUTOGEN.md, command reference, and action bro…
typeagent-bot[bot] Jul 21, 2026
2c30376
Refactor: dedupe input-event dispatch, drop dead isCopy var
GeorgeNgMsft Jul 21, 2026
571347d
Merge branch 'dev/georgeng/vscode_copy' into dev/georgeng/reasoning_a…
GeorgeNgMsft Jul 21, 2026
783ea6a
Merge branch 'dev/georgeng/reasoning_action_ui' of https://github.com…
GeorgeNgMsft Jul 21, 2026
658b843
docs: regenerate README.AUTOGEN.md, command reference, and action bro…
typeagent-bot[bot] Jul 21, 2026
6947ca8
Merge remote-tracking branch 'origin/main' into _heal
typeagent-bot[bot] Jul 21, 2026
fb98e3d
Merge branch 'main' into dev/georgeng/reasoning_action_ui
GeorgeNgMsft Jul 21, 2026
7d7f3cc
Merge branch 'main' into dev/georgeng/reasoning_action_ui
GeorgeNgMsft Jul 21, 2026
8ec4511
Merge remote-tracking branch 'origin/main' into _heal
typeagent-bot[bot] Jul 21, 2026
3f128cb
docs: regenerate README.AUTOGEN.md, command reference, and action bro…
typeagent-bot[bot] Jul 22, 2026
2f86f9c
Rename expand to openInNewWindow and clean up unused chat-ui changes
GeorgeNgMsft Jul 22, 2026
0b06d6a
Merget branch 'dev/georgeng/reasoning_action_ui' of https://github.c…
GeorgeNgMsft Jul 22, 2026
9ab74c3
docs: regenerate README.AUTOGEN.md, command reference, and action bro…
typeagent-bot[bot] Jul 22, 2026
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
14 changes: 7 additions & 7 deletions ts/packages/chat-ui/README.AUTOGEN.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<!-- AUTOGEN:DOCS:START -->

<!-- AUTOGEN:DOCS:HASH:sha256=7191e8d7c94101c950d1de2226b1ff176f85c6ba27a92d65fe1fb2bd14edf01d -->
<!-- AUTOGEN:DOCS:HASH:sha256=64bf1c607a43b00bcabebf7ffcf8af46af0d2498888c1f6576ff46ca3c7a14fb -->
<!-- AUTOGEN:DOCS:SOURCE: ./README.md (hand-written documentation; this file is the AI-generated companion) -->

# chat-ui — AI-generated documentation
Expand All @@ -12,13 +12,13 @@

## Overview

The `chat-ui` package provides a shared, framework-free chat user interface for TypeAgent applications. It is designed to deliver a consistent and interactive chat experience across multiple platforms, including the VS Code shell extension, the browser extension chat panel, and the Visual Studio extension webview. The package includes components for rendering user and agent messages, handling streaming updates, replaying chat history, managing connection status, and collecting user feedback.
The `chat-ui` package provides a shared, framework-free chat user interface for TypeAgent applications. It is designed to ensure a consistent and interactive chat experience across multiple platforms, including the VS Code shell extension, the browser extension chat panel, and the Visual Studio extension webview. The package includes components for rendering user and agent messages, handling streaming updates, replaying chat history, managing connection status, and collecting user feedback.

## What it does

The `chat-ui` package offers a range of features to support dynamic and interactive chat interfaces:
The `chat-ui` package offers a set of tools and components to build and manage chat interfaces. Its primary features include:

- **ChatPanel**: The primary component for rendering the chat interface. It supports:
- **ChatPanel**: The core component for rendering the chat interface. It supports:

- Adding user and agent messages via `addAgentMessage`.
- Updating display metadata with `setDisplayInfo`.
Expand All @@ -35,7 +35,7 @@ The `chat-ui` package offers a range of features to support dynamic and interact

- **Shared Styles**: A CSS file (`styles/chat.css`) is included to provide a consistent appearance for the chat UI across all host applications.

The package is used by several TypeAgent components, such as the VS Code shell, the browser extension, and the Visual Studio extension webview.
The package is used by several TypeAgent components, such as the VS Code shell, the browser extension, and the Visual Studio extension webview, ensuring a unified user experience across these platforms.

## Setup

Expand All @@ -56,7 +56,7 @@ For additional details on usage and integration, refer to the hand-written READM

## Key Files

The `chat-ui` package is structured into several key files, each responsible for specific functionality:
The `chat-ui` package is organized into several key files, each responsible for specific functionality:

- **[index.ts](./src/index.ts)**: The main entry point, exporting the package's primary components and utilities.
- **[chatPanel.ts](./src/chatPanel.ts)**: Implements the `ChatPanel` component, which is the core of the chat UI. It handles user input, agent messages, and display updates.
Expand Down Expand Up @@ -120,6 +120,6 @@ External: `ansi_up`, `dompurify`, `markdown-it`

---

_Auto-generated against commit `abfa7f206e497772254557b7d65cf9252a4728b2` on `2026-07-21T21:21:02.587Z` by `docs-generate.yml`. Links validated at that commit; the working tree may have drifted by up to 24h. Re-run `pnpm --filter chat-ui docs:verify-links` to spot-check._
_Auto-generated against commit `0b06d6a1cc9d93888e91e217057d9c148b3cc49f` on `2026-07-22T04:45:01.388Z` by `docs-generate.yml`. Links validated at that commit; the working tree may have drifted by up to 24h. Re-run `pnpm --filter chat-ui docs:verify-links` to spot-check._

<!-- AUTOGEN:DOCS:END -->
111 changes: 110 additions & 1 deletion ts/packages/chat-ui/src/chatPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1874,11 +1874,17 @@ export class ChatPanel {
*/
private clearAgentRunning(threadId: string): void {
this.agentRunningRequestIds.delete(threadId);
this.threadContainers.get(threadId)?.clearRunning();
// The request finished: collapse every step's reasoning "Thinking"
// block (including the last, still-expanded one) so completed reasoning
// minimizes.
const current = this.threadContainers.get(threadId);
current?.clearRunning();
current?.collapseReasoning();
const all = this.requestAgentContainers.get(threadId);
if (all) {
for (const container of all) {
container.clearRunning();
container.collapseReasoning();
}
}
}
Expand Down Expand Up @@ -2541,6 +2547,10 @@ export class ChatPanel {
source.startsWith("dispatcher.reasoningAction")
) {
stepContainer.overrideSource(source, sourceIcon);
// Also covers a reused existing bubble (e.g. a generic
// "Executing action" temporary) that never passed through the
// reasoning detection in getOrCreateAgentContainer.
stepContainer.markReasoning();
}
if (requestId) {
const start = this.requestStartByRequestId.get(requestId);
Expand All @@ -2560,6 +2570,7 @@ export class ChatPanel {
for (const prior of priorSteps) {
if (prior !== stepContainer) {
prior.clearRunning();
prior.collapseReasoning();
}
}
}
Expand Down Expand Up @@ -2630,8 +2641,17 @@ export class ChatPanel {
requestId: string | undefined,
): AgentMessageContainer {
const threadId = this.resolveThreadId(requestId);
// A reasoning display carries a "dispatcher.reasoningAction.*" source;
// mark the bubble so its working rail reads as reasoning (purple)
// whether the content streams (temporary), commits as a step, or
// arrives as a block.
const isReasoning =
source?.startsWith("dispatcher.reasoningAction") ?? false;
const existing = this.threadContainers.get(threadId);
if (existing) {
if (isReasoning) {
existing.markReasoning();
}
return existing;
}
// sourceIcon="🤖"). Without this, the bubble would be created with
Expand Down Expand Up @@ -2670,6 +2690,9 @@ export class ChatPanel {
anchor,
);
this.threadContainers.set(threadId, container);
if (isReasoning) {
container.markReasoning();
}
container.div.dataset.requestId = threadId;
if (this.developerMode) {
this.attachDeleteControl(container.div, threadId, "agent");
Expand Down Expand Up @@ -3722,6 +3745,17 @@ export class ChatPanel {
(requestContainers.length > 0
? requestContainers[requestContainers.length - 1]
: undefined);
// For a reasoning thread with a trail of "Thinking"/tool steps, give
// the final answer step a gap separating it from the reasoning above.
// Target the last committed STEP (the answer), not the newest
// container, which can be a post-answer status block (e.g. copilot's
// "Task flow registered").
const answerStep = requestContainers.find(
(c) => c.div === this.lastStepAnchorByThread.get(threadId),
);
if (requestContainers.length > 1 && answerStep?.isReasoning()) {
answerStep.setReasoningAnswer(true);
}
const firstMessageMs = this.firstMessageMsByRequestId.get(threadId);
if (result?.cancelled) {
// Mirror Electron's "⚠ Cancelled" status, anchored to the
Expand Down Expand Up @@ -5188,6 +5222,8 @@ class AgentMessageContainer {
private readonly timestampDiv: HTMLDivElement;
private feedbackWidget?: FeedbackWidget;
private statusRail?: HTMLDivElement;
private reasoning = false;
private readonly collapsedReasoning = new WeakSet<HTMLDetailsElement>();
private lastAppendMode?: DisplayAppendMode;
// Mirrors the shell's swapContent pattern: when action JSON is set,
// clicking the agent name toggles the message body between the
Expand Down Expand Up @@ -5593,12 +5629,31 @@ class AgentMessageContainer {
bodyDiv: this.messageDiv,
headerDiv: this.timestampDiv,
messageDiv: this.messageDiv,
openInWindow: this.platformAdapter.openMessageInWindow
? () => this.openMessageInWindow()
: undefined,
},
controller,
variant,
);
}

// Ask the host to open this message in a new window. Messages backed by an
// <iframe> (script-driven image/video/settings displays) don't survive
// re-sanitization in a separate panel, so decline them; there's no in-page
// fallback, so the button is simply inert for those.
private openMessageInWindow(): boolean {
if (this.messageDiv.querySelector("iframe")) {
return false;
}
return (
this.platformAdapter.openMessageInWindow?.(
this.messageDiv.innerHTML,
this.nameSpan.textContent ?? undefined,
) ?? false
);
}

public setFeedbackState(entry: UserFeedbackEntry | null) {
this.feedbackWidget?.setFeedbackState(entry);
}
Expand Down Expand Up @@ -5634,6 +5689,11 @@ class AgentMessageContainer {
// Ensure the rail reads as "running" even if it was previously showing
// the muted "unknown" state (setUnknown reuses this same rail element).
rail.dataset.status = "running";
if (this.reasoning) {
rail.dataset.variant = "reasoning";
} else {
delete rail.dataset.variant;
}
const stateZone = rail.querySelector<HTMLElement>(
":scope > .chat-status-state-zone",
)!;
Expand Down Expand Up @@ -5675,6 +5735,55 @@ class AgentMessageContainer {
this.statusRail = undefined;
}

/**
* Mark this bubble as part of a reasoning ("Thinking") flow so its working
* rail reads as reasoning (light purple) rather than a generic running
* request (blue). Updates an already-rendered rail immediately, since the
* rail is stamped before the reasoning source is known.
*/
public markReasoning() {
this.reasoning = true;
if (this.statusRail) {
this.statusRail.dataset.variant = "reasoning";
}
}

/** Whether this bubble belongs to a reasoning ("Thinking") flow. */
public isReasoning(): boolean {
return this.reasoning;
}

/**
* Collapse this step's reasoning "Thinking" blocks once it is no longer
* active (superseded by the next step, or the request completed) so
* finished reasoning minimizes while the active step stays expanded. Each
* block is collapsed at most once - tracked per element - so a block the
* user manually re-opens is not re-collapsed by a later pass, while a block
* added after an earlier collapse still minimizes.
*/
public collapseReasoning() {
this.bodyDiv
.querySelectorAll<HTMLDetailsElement>(
"details.reasoning-thinking[open]",
)
.forEach((details) => {
if (this.collapsedReasoning.has(details)) {
return;
}
this.collapsedReasoning.add(details);
details.removeAttribute("open");
});
}

/**
* Tag this reasoning step as the final prose answer rather than a
* "Thinking"/tool trail step, so it renders with a gap separating it from
* the reasoning above it.
*/
public setReasoningAnswer(isAnswer: boolean) {
this.div.classList.toggle("chat-reasoning-answer", isAnswer);
}

/**
* Swap the "working" rail for a muted "status unknown" state: no spinner,
* no Stop button. Used when the connection drops mid-request — we can't
Expand Down
38 changes: 34 additions & 4 deletions ts/packages/chat-ui/src/feedbackWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
// Licensed under the MIT License.

/**
* Feedback widget. Mirrors packages/shell/src/renderer/src/feedbackWidget.ts —
* keep these in sync when changing trigger DOM, popover layout, or the
* CSS class names so the styling can stay shared between the Electron
* shell and the Chrome / vscode webview hosts.
* Feedback widget. Renders the per-message action row (copy / open-in-window /
* rate) plus the feedback popover. Shared by the Electron shell (via chat-ui's
* ChatPanel) and the Chrome / vscode webview hosts, so keep the trigger DOM and
* CSS class names stable when changing it.
*/

import type {
Expand All @@ -16,6 +16,7 @@ import type {
import {
iconCheck,
iconCopy,
iconOpenInWindow,
iconMore,
iconThumbsDown,
iconThumbsUp,
Expand All @@ -40,6 +41,15 @@ type FeedbackHost = {
bodyDiv: HTMLElement;
headerDiv: HTMLElement;
messageDiv: HTMLElement;
/**
* Optional host-native "open in new window" (e.g. a movable VS Code editor
* panel). When provided, the widget shows the "Open in new window" action;
* hosts without it don't show the action at all. Returns true if the host
* handled it; a host may decline a specific message (false, e.g. iframe-
* backed content that can't be serialized into a window), in which case
* nothing happens (there's no in-page fallback).
*/
openInWindow?: () => boolean;
};

type ActionRow = {
Expand Down Expand Up @@ -111,6 +121,26 @@ export class FeedbackWidget {
() => this.copyMessage(copyBtn),
);
root.appendChild(copyBtn);

// "Open in new window" is an opt-in, host-native capability: only
// render it when the host provides openInWindow (e.g. the VS Code
// shell, which opens a movable editor panel). Hosts without it (the
// Electron shell, web) show no button. The host may decline a
// specific message (returns false, e.g. iframe-backed content that
// can't be serialized into a window); there's no in-page fallback,
// so the click is simply a no-op in that case.
const openInWindow = this.host.openInWindow;
if (openInWindow) {
const openWindowBtn = makeIconButton(
"open-window",
"Open in new window",
iconOpenInWindow(),
() => {
openInWindow();
},
);
root.appendChild(openWindowBtn);
}
}

const thumbsUp = makeIconButton(
Expand Down
8 changes: 8 additions & 0 deletions ts/packages/chat-ui/src/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,11 @@ export function iconStop() {
`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="none" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"></rect></svg>`,
);
}

export function iconOpenInWindow() {
// "maximize" window: four corner brackets forming a frame; reads as
// "open in a new window".
return fromSvg(
`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="4 9 4 4 9 4"></polyline><polyline points="15 4 20 4 20 9"></polyline><polyline points="20 15 20 20 15 20"></polyline><polyline points="9 20 4 20 4 15"></polyline></svg>`,
);
}
11 changes: 11 additions & 0 deletions ts/packages/chat-ui/src/platformAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@
export interface PlatformAdapter {
/** Handle a click on an <a> link in rendered content. */
handleLinkClick(href: string, target: string | null): void;

/**
* Open a message's content (an HTML string, plus an optional title) in a
* host-native new window, e.g. a movable VS Code editor panel. Return true
* if handled. This is an opt-in capability: chat-ui only shows the "open in
* new window" action when a host provides it. A host may decline a specific
* message (return false, e.g. iframe-backed content that can't be
* serialized into a window); there's no in-page fallback, so the click is
* simply a no-op in that case.
*/
openMessageInWindow?(html: string, title?: string): boolean;
}

/**
Expand Down
Loading
Loading