sessions: change session description to IMarkdownString and render markdown (#306468)
Some checks failed
Monaco Editor checks / Monaco Editor checks (push) Has been cancelled
Code OSS (node_modules) / Compile (push) Has been cancelled
Code OSS (node_modules) / Linux (push) Has been cancelled
Code OSS (node_modules) / macOS (push) Has been cancelled
Code OSS (node_modules) / Windows (push) Has been cancelled
Checking Component Screenshots / Checking Component Screenshots (push) Has been cancelled

* sessions: change description type to IMarkdownString and render it

- Change ISessionData, IChat, and ISession description field types from
  string | undefined to IMarkdownString | undefined
- Update all provider implementations (CopilotCLISession, RemoteNewSession,
  AgentSessionAdapter, RemoteSessionAdapter) to use IMarkdownString
- In AgentSessionAdapter._extractDescription, wrap plain strings in
  MarkdownString; IMarkdownString values are passed through as-is
- In sessionsList.ts, render markdown descriptions using markdownRendererService
  with sanitizerConfig: { replaceWithPlaintext: true } — matching the existing
  agentSessionsViewer.ts pattern; fallback status labels use plain textContent
- Add MutableDisposable to track the markdown render lifecycle so it is
  properly cleared when status changes
- Update CSS to handle paragraph elements inside rendered markdown
- Update SESSIONS_PROVIDER.md documentation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* sessions: clear descriptionDisposable on plain-text fallback to avoid leaks

When the status is active (InProgress/NeedsInput/Error) but description
becomes undefined, the previous markdown renderer was left alive even though
its container element had been removed from the DOM by the autorun rebuild.

Fix: call descriptionDisposable.clear() in every else-branch (plain-text
fallback) so any prior IMarkdownRendererService render is disposed before
the new plain-text content is set.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* sessions: fix import of IsNewChatSessionContext after move to common/contextkeys.ts

Upstream commit moved IsNewChatSessionContext from sessionsManagementService.ts
to the centralised common/contextkeys.ts. Update the import in
sessionsTitleBarWidget.ts accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Sandeep Somavarapu
2026-03-30 23:16:28 +02:00
committed by GitHub
parent 4ebc211cda
commit 0930f054ee
7 changed files with 49 additions and 21 deletions

View File

@@ -65,7 +65,7 @@ The common session interface exposed by all providers. It is a self-contained fa
| `loading` | `IObservable<boolean>` | Whether the session is initializing |
| `isArchived` | `IObservable<boolean>` | Archive state |
| `isRead` | `IObservable<boolean>` | Read/unread state |
| `description` | `IObservable<string \| undefined>` | Status description (e.g., current agent action) |
| `description` | `IObservable<IMarkdownString \| undefined>` | Status description (e.g., current agent action), supports markdown |
| `lastTurnEnd` | `IObservable<Date \| undefined>` | When the last agent turn ended |
| `pullRequest` | `IObservable<ISessionPullRequest \\| undefined>` | Associated pull request |

View File

@@ -5,6 +5,7 @@
import { Emitter, Event } from '../../../../base/common/event.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js';
import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
import { IObservable, observableValue, transaction } from '../../../../base/common/observable.js';
import { themeColorFromId, ThemeIcon } from '../../../../base/common/themables.js';
@@ -77,8 +78,8 @@ export class CopilotCLISession extends Disposable implements ISessionData {
private readonly _title = observableValue(this, '');
readonly title: IObservable<string> = this._title;
private readonly _description: ReturnType<typeof observableValue<string | undefined>>;
readonly description: IObservable<string | undefined>;
private readonly _description: ReturnType<typeof observableValue<IMarkdownString | undefined>>;
readonly description: IObservable<IMarkdownString | undefined>;
private readonly _updatedAt = observableValue(this, new Date());
readonly updatedAt: IObservable<Date> = this._updatedAt;
@@ -381,7 +382,7 @@ export class RemoteNewSession extends Disposable implements ISessionData {
readonly isArchived: IObservable<boolean> = observableValue(this, false);
readonly isRead: IObservable<boolean> = observableValue(this, true);
readonly description: IObservable<string | undefined> = observableValue(this, undefined);
readonly description: IObservable<IMarkdownString | undefined> = observableValue(this, undefined);
readonly lastTurnEnd: IObservable<Date | undefined> = observableValue(this, undefined);
readonly gitHubInfo: IObservable<IGitHubInfo | undefined> = observableValue(this, undefined);
@@ -623,8 +624,8 @@ class AgentSessionAdapter implements ISessionData {
private readonly _isRead: ReturnType<typeof observableValue<boolean>>;
readonly isRead: IObservable<boolean>;
private readonly _description: ReturnType<typeof observableValue<string | undefined>>;
readonly description: IObservable<string | undefined>;
private readonly _description: ReturnType<typeof observableValue<IMarkdownString | undefined>>;
readonly description: IObservable<IMarkdownString | undefined>;
private readonly _lastTurnEnd: ReturnType<typeof observableValue<Date | undefined>>;
readonly lastTurnEnd: IObservable<Date | undefined>;
@@ -703,11 +704,11 @@ class AgentSessionAdapter implements ISessionData {
}
}
private _extractDescription(session: IAgentSession): string | undefined {
private _extractDescription(session: IAgentSession): IMarkdownString | undefined {
if (!session.description) {
return undefined;
}
return typeof session.description === 'string' ? session.description : session.description.value;
return typeof session.description === 'string' ? new MarkdownString(session.description) : session.description;
}
private _extractGitHubInfo(session: IAgentSession): IGitHubInfo | undefined {

View File

@@ -6,6 +6,7 @@
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js';
import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
import { basename } from '../../../../base/common/resources.js';
import { ISettableObservable, observableValue } from '../../../../base/common/observable.js';
@@ -54,7 +55,7 @@ class RemoteSessionAdapter implements ISessionData {
readonly loading = observableValue('loading', false);
readonly isArchived = observableValue('isArchived', false);
readonly isRead = observableValue('isRead', true);
readonly description: ISettableObservable<string | undefined>;
readonly description: ISettableObservable<IMarkdownString | undefined>;
readonly lastTurnEnd: ISettableObservable<Date | undefined>;
readonly gitHubInfo = observableValue<IGitHubInfo | undefined>('gitHubInfo', undefined);
@@ -79,7 +80,7 @@ class RemoteSessionAdapter implements ISessionData {
this.title = observableValue('title', metadata.summary ?? `Session ${rawId.substring(0, 8)}`);
this.updatedAt = observableValue('updatedAt', new Date(metadata.modifiedTime));
this.lastTurnEnd = observableValue('lastTurnEnd', metadata.modifiedTime ? new Date(metadata.modifiedTime) : undefined);
this.description = observableValue('description', providerLabel);
this.description = observableValue('description', new MarkdownString().appendText(providerLabel));
this.workspace = observableValue('workspace', metadata.workingDirectory
? RemoteAgentHostSessionsProvider.buildWorkspace(metadata.workingDirectory, providerLabel, connectionAuthority)
: undefined);

View File

@@ -204,6 +204,13 @@
text-overflow: ellipsis;
}
.session-description p {
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.session-description:empty {
display: none;
}

View File

@@ -21,11 +21,11 @@ import { IWorkbenchLayoutService, Parts } from '../../../../workbench/services/l
import { Menus } from '../../../browser/menus.js';
import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js';
import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js';
import { ISessionsManagementService, IsNewChatSessionContext } from './sessionsManagementService.js';
import { ISessionsManagementService } from './sessionsManagementService.js';
import { autorun, observableSignalFromEvent } from '../../../../base/common/observable.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { IsAuxiliaryWindowContext } from '../../../../workbench/common/contextkeys.js';
import { ChatSessionProviderIdContext, SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js';
import { ChatSessionProviderIdContext, IsNewChatSessionContext, SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js';
import { ISessionsProvidersService } from './sessionsProvidersService.js';
import { SessionStatus } from '../common/sessionData.js';
import { SHOW_SESSIONS_PICKER_COMMAND_ID } from './sessionsActions.js';

View File

@@ -251,6 +251,7 @@ class SessionItemRenderer implements ITreeRenderer<SessionListItem, FuzzyScore,
// Details row — reactive: badge · diff stats · time
const timeDisposable = template.elementDisposables.add(new MutableDisposable());
const descriptionDisposable = template.elementDisposables.add(new MutableDisposable());
template.elementDisposables.add(autorun(reader => {
const sessionStatus = element.status.read(reader);
const changes = element.changes.read(reader);
@@ -317,22 +318,39 @@ class SessionItemRenderer implements ITreeRenderer<SessionListItem, FuzzyScore,
DOM.append(template.detailsRow, $('span.session-separator.has-separator'));
}
const statusEl = DOM.append(template.detailsRow, $('span.session-description'));
statusEl.textContent = description ?? localize('working', "Working...");
if (description) {
descriptionDisposable.value = this.markdownRendererService.render(description, { sanitizerConfig: { replaceWithPlaintext: true } }, statusEl);
} else {
descriptionDisposable.clear();
statusEl.textContent = localize('working', "Working...");
}
parts.push(statusEl);
} else if (sessionStatus === SessionStatus.NeedsInput) {
if (parts.length > 0) {
DOM.append(template.detailsRow, $('span.session-separator.has-separator'));
}
const statusEl = DOM.append(template.detailsRow, $('span.session-description'));
statusEl.textContent = description ?? localize('needsInput', "Input needed");
if (description) {
descriptionDisposable.value = this.markdownRendererService.render(description, { sanitizerConfig: { replaceWithPlaintext: true } }, statusEl);
} else {
descriptionDisposable.clear();
statusEl.textContent = localize('needsInput', "Input needed");
}
parts.push(statusEl);
} else if (sessionStatus === SessionStatus.Error) {
if (parts.length > 0) {
DOM.append(template.detailsRow, $('span.session-separator.has-separator'));
}
const statusEl = DOM.append(template.detailsRow, $('span.session-description'));
statusEl.textContent = localize('failed', "Failed");
if (description) {
descriptionDisposable.value = this.markdownRendererService.render(description, { sanitizerConfig: { replaceWithPlaintext: true } }, statusEl);
} else {
descriptionDisposable.clear();
statusEl.textContent = localize('failed', "Failed");
}
parts.push(statusEl);
} else {
descriptionDisposable.clear();
}
// Timestamp — visible when not hiding details

View File

@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IMarkdownString } from '../../../../base/common/htmlContent.js';
import { IObservable } from '../../../../base/common/observable.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { URI } from '../../../../base/common/uri.js';
@@ -116,8 +117,8 @@ export interface ISessionData {
readonly isArchived: IObservable<boolean>;
/** Whether the session has been read. */
readonly isRead: IObservable<boolean>;
/** Status description shown while the session is active (e.g., current agent action). */
readonly description: IObservable<string | undefined>;
/** Status description shown while the session is active (e.g., current agent action). Supports markdown. */
readonly description: IObservable<IMarkdownString | undefined>;
/** Timestamp of when the last agent turn ended, if any. */
readonly lastTurnEnd: IObservable<Date | undefined>;
/** GitHub information associated with this session, if any. */
@@ -164,8 +165,8 @@ export interface IChat {
readonly isArchived: IObservable<boolean>;
/** Whether the chat has been read. */
readonly isRead: IObservable<boolean>;
/** Status description shown while the chat is active (e.g., current agent action). */
readonly description: IObservable<string | undefined>;
/** Status description shown while the chat is active (e.g., current agent action). Supports markdown. */
readonly description: IObservable<IMarkdownString | undefined>;
/** Timestamp of when the last agent turn ended, if any. */
readonly lastTurnEnd: IObservable<Date | undefined>;
/** GitHub information associated with this session, if any. */
@@ -212,8 +213,8 @@ export interface ISession {
readonly isArchived: IObservable<boolean>;
/** Whether the session has been read. */
readonly isRead: IObservable<boolean>;
/** Status description shown while the session is active (e.g., current agent action). */
readonly description: IObservable<string | undefined>;
/** Status description shown while the session is active (e.g., current agent action). Supports markdown. */
readonly description: IObservable<IMarkdownString | undefined>;
/** Timestamp of when the last agent turn ended, if any. */
readonly lastTurnEnd: IObservable<Date | undefined>;
/** GitHub information associated with this session, if any. */