Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .github/CODENOTIFY
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,8 @@ test/sanity/** @dmitrivMS
# Agents Workbench
# Ensure the Agents workbench team is aware of changes to the sessions model and services
src/vs/sessions/services/sessions/** @sandy081 @lszomoru

# Ensure the Agents workbench team is aware of changes to the core workbench layout
src/vs/sessions/contrib/layout/** @benibenj
src/vs/sessions/browser/parts/** @benibenj
src/vs/sessions/browser/workbench.ts @benibenj
799 changes: 799 additions & 0 deletions build/azure-pipelines/product-build optional-TSA.yml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export class ExtensionContributedChatEndpoint implements IChatEndpoint {
public readonly multiplier: number | undefined = undefined;
public readonly isExtensionContributed = true;
public readonly supportedEditTools?: readonly EndpointEditToolName[] | undefined;
// Extension-contributed endpoints are not backed by the CAPI Copilot token; treat them as owning auth to opt out of token fallback and related tool gating.
public readonly ownsAuthorization = true;

constructor(
private readonly languageModel: vscode.LanguageModelChat,
Expand Down
2 changes: 1 addition & 1 deletion src/vs/sessions/LAYOUT.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ Key invariants:
- **Slot reuse on reconcile.** `SessionsPart.updateVisibleSessions` grows or shrinks its internal pool of `SessionView`s to match the visible count, then rebinds each surviving slot to its session by position via `SessionView.openSession(session)`. Slots are never destroyed and recreated for an existing session — only added at the right or popped from the right when the count changes.
- **Focus promotes to active.** Focus-in or pointer-down on a non-placeholder session view promotes that session to active (via `onDidFocusSession` → `ISessionsManagementService.setActive`).
- **Maximize.** When two or more non-placeholder views are visible, the active view can be maximized within the part's internal grid; the part exposes `toggleMaximizeSession(sessionId)`.
- **Restored on reload.** The visibility model is persisted to workspace storage (order, sticky state, and which slot is active, including the empty new-session slot). On startup `ISessionsManagementService.restoreVisibleSessions()` rebuilds the grid, waiting for each session's provider to make it available and re-applying order, sticky flags, and the active session. To avoid flicker, restore waits for the active session, then lays out all sessions that are already available in one atomic transaction (`VisibleSessions.restoreGrid`) rather than showing the active session alone and reflowing as siblings load. Sessions whose provider surfaces them later are inserted into their persisted position incrementally. Once the grid has been laid out, keyboard focus is moved into the restored active session (matching the behaviour when a session is opened explicitly) so the user can start typing immediately. Focus is driven by the part service observing `ISessionsManagementService.activeSession` rather than the management service calling into the view. The move is guarded so it never steals focus from another surface: focus is pulled into a session only when it currently rests on `<body>`/nothing (startup restore) or already within the grid (moving between leaves), so an incidental active-session change (e.g. the fallback after deleting a session from the list) does not yank focus out of the list. Deliberate opens originating elsewhere move focus via their own explicit `focusSession` call. While restore runs, the **part service** suppresses the empty new-session view (it owns a `restoring` flag and hides the grid so the composer does not flash before the restored sessions appear). The workbench brackets its call to `restoreVisibleSessions()` with `ISessionsPartService.beginSessionRestore()` / `endSessionRestore()`; because startup runs the part `init()` and `restore()` in the same synchronous task, the suppression is in effect before the first paint, and a fresh start (nothing to restore) clears it within a microtask so the composer is not blanked. A safety timeout guarantees the new-session view is never suppressed indefinitely if a persisted session fails to resurface. The restoring state is intentionally a view-layer concern: the management service exposes no UI suppression flag. (Restore itself drives no part-wide progress; once a session's leaf is laid out, that leaf shows its own load progress as described above.)
- **Restored on reload.** The visibility model is persisted to workspace storage (order, sticky state, and which slot is active, including the empty new-session slot). On startup `ISessionsManagementService.restoreVisibleSessions()` rebuilds the grid, waiting for each session's provider to make it available and re-applying order, sticky flags, and the active session. To avoid flicker, restore waits for the active session, then lays out all sessions that are already available in one atomic transaction (`VisibleSessions.restoreGrid`) rather than showing the active session alone and reflowing as siblings load. Sessions whose provider surfaces them later are inserted into their persisted position incrementally. Once the grid has been laid out, keyboard focus is moved into the restored active session (matching the behaviour when a session is opened explicitly) so the user can start typing immediately. Focus is driven by the part service observing `ISessionsManagementService.activeSession` rather than the management service calling into the view. The move is guarded so it never steals focus from another surface: focus is pulled into a session only when it currently rests on `<body>`/nothing (startup restore) or already within the grid (moving between leaves), so an incidental active-session change (e.g. the fallback after deleting a session from the list) does not yank focus out of the list. Deliberate opens originating elsewhere move focus via their own explicit `focusSession` call. Restore must win the race against the empty new-session slot, whose workspace picker resolves asynchronously on the same provider-registration event restore waits for and would otherwise create and activate an untitled draft. Three mechanisms guarantee restore wins: (1) `ISessionsManagementService` is registered **eagerly** so the restore wiring and visibility model are alive before the first paint; (2) when restore rebinds the placeholder slot to the restored session, the new-session view (and its `NewChatWidget`) is disposed, and `NewChatWidget` guards its async workspace-selection handler with `this._store.isDisposed` so a late-resolving picker cannot create a draft for a slot that has already been claimed by a restored session; (3) untitled drafts are never persisted — `restoreVisibleSessions` drops them from the snapshot (`_snapshotVisibleSessionStates`) — so a stale draft can never be restored. The restoring state is intentionally not a UI suppression flag: the management service exposes none. (Restore itself drives no part-wide progress; once a session's leaf is laid out, that leaf shows its own load progress as described above.)

### 4.3 Mobile / Phone

Expand Down
1 change: 0 additions & 1 deletion src/vs/sessions/browser/menus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,4 @@ export const Menus = {
NewSessionRepositoryConfig: new MenuId('NewSessions.RepositoryConfigMenu'),
SessionWorkspaceManage: new MenuId('Sessions.SessionWorkspaceManage'),
SessionBarToolbar: new MenuId('SessionsSessionBarToolbar'),
SessionBarInlineToolbar: new MenuId('SessionsSessionBarInlineToolbar'),
} as const;
9 changes: 1 addition & 8 deletions src/vs/sessions/browser/parts/media/chatCompositeBar.css
Original file line number Diff line number Diff line change
Expand Up @@ -164,21 +164,14 @@
gap: 0;
}

.chat-composite-bar-inline-toolbar {
display: flex;
align-items: center;
flex-shrink: 0;
}

.chat-composite-bar-toolbar {
display: flex;
align-items: center;
flex-shrink: 0;
}

/* Keep header toolbar buttons from dictating the row height */
.chat-composite-bar-title-actions .action-item .action-label,
.chat-composite-bar-meta-row .chat-composite-bar-inline-toolbar .action-item .action-label {
.chat-composite-bar-title-actions .action-item .action-label {
height: 20px;
line-height: 20px;
padding: 0 2px;
Expand Down
2 changes: 0 additions & 2 deletions src/vs/sessions/browser/parts/media/sessionView.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
* smoothly in either direction. Only declared when the user has not requested
* reduced motion so we never need to unset them. */
@media (prefers-reduced-motion: no-preference) {
.session-view .chat-composite-bar-inline-toolbar,
.session-view .chat-composite-bar-toolbar,
.session-view .interactive-session .chat-input-toolbars > .chat-input-toolbar,
.session-view .interactive-session .chat-secondary-toolbar,
Expand All @@ -32,7 +31,6 @@

/* Quiet the session toolbar controls in the top-right of inactive sessions so
* the active session's controls stand out, while keeping them legible. */
.session-view:not(.is-active) .chat-composite-bar-inline-toolbar,
.session-view:not(.is-active) .chat-composite-bar-toolbar {
opacity: 0.6;
}
Expand Down
5 changes: 0 additions & 5 deletions src/vs/sessions/browser/parts/media/sessionsPart.css
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,6 @@
z-index: 10;
}

/* While sessions are being restored on startup, hide the grid so the empty new-session view does not flash before the restored sessions are laid out. The progress bar (a sibling above) stays visible to indicate loading. */
.monaco-workbench .part.sessionspart > .content > .restoring {
visibility: hidden;
}

.monaco-workbench .part.sessionspart .title-actions .actions-container {
justify-content: flex-end;
}
Expand Down
50 changes: 21 additions & 29 deletions src/vs/sessions/browser/parts/media/titlebarpart.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,31 @@
.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-center {
flex: 0 1 auto;
max-width: none;
display: flex;
/* Grid with symmetric 1fr side tracks keeps the command center (auto track)
* window-centered. When the right actions container widens on hover (e.g.
* "Open in VS Code" label animating in), the left 1fr track grows to mirror
* it so the center track does not shift. */
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
justify-content: center;
gap: 4px;
column-gap: 4px;
min-width: 0;
}

.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-center > .titlebar-center-nav-container {
grid-column: 1;
justify-self: end;
}

.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-center > .window-title {
grid-column: 2;
}

.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-center > .titlebar-center-actions-container {
grid-column: 3;
justify-self: start;
}

.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-center .window-title {
margin: unset;
/* Match the VS Code window command center font size. */
Expand All @@ -69,32 +87,6 @@
display: flex;
}

/* The right-hand center actions (Open in VS Code) take no horizontal space in the
* centered cluster: the container is zero-width and overflows to the right. This way
* the widget's hover expansion grows rightward without widening the cluster, so the
* window-centered command center box never gets pushed. */
.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-center > .titlebar-center-actions-container {
width: 0;
flex: 0 0 0;
overflow: visible;
/* The buttons overflow rightward into the area painted by .titlebar-right (a
* later, static drag region). Lift this container above it so the overflowing
* actions (Run, Open in VS Code) stay interactive — matching .left-toolbar-container. */
position: relative;
z-index: 2500;
-webkit-app-region: no-drag;
}

/* The container itself is zero-width, so its -webkit-app-region: no-drag carves out a
* zero-width hole and the overflowing buttons would otherwise sit inside the titlebar's
* OS drag region (no pointer cursor, swallowed clicks). Re-declare no-drag on the action
* items, which have real content width, so each button gets a proper no-drag hit region.
* (The Open in VS Code widget already does this on its own element; this covers Run and
* any other action hosted here.) */
.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-center > .titlebar-center-actions-container .monaco-action-bar .action-item {
-webkit-app-region: no-drag;
}

.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-center > .titlebar-actions-container .monaco-action-bar .action-item:not(.disabled) .codicon {
color: var(--vscode-icon-foreground);
}
Expand Down
25 changes: 8 additions & 17 deletions src/vs/sessions/browser/parts/sessionHeader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ export class SessionHeader extends Disposable {
private readonly _titleEl: HTMLElement;
private readonly _metaRow: HTMLElement;
private readonly _toolbar: MenuWorkbenchToolBar;
private readonly _inlineToolbar: MenuWorkbenchToolBar;
private readonly _inlineToolbarContainer: HTMLElement;
private readonly _titleActionsEl: HTMLElement;

private readonly _sessionDisposables = this._register(new MutableDisposable<DisposableStore>());
Expand Down Expand Up @@ -99,19 +97,15 @@ export class SessionHeader extends Disposable {
titleRow.appendChild(titleActions);
this._titleActionsEl = titleActions;

this._inlineToolbarContainer = $('.chat-composite-bar-inline-toolbar');
this._inlineToolbar = this._register(instantiationService.createInstance(MenuWorkbenchToolBar, this._inlineToolbarContainer, Menus.SessionBarInlineToolbar, {
hiddenItemStrategy: HiddenItemStrategy.Ignore,
menuOptions: { shouldForwardArgs: true },
highlightToggledItems: true,
}));

const toolbarContainer = $('.chat-composite-bar-toolbar');
titleActions.appendChild(toolbarContainer);
this._toolbar = this._register(instantiationService.createInstance(MenuWorkbenchToolBar, toolbarContainer, Menus.SessionBarToolbar, {
hiddenItemStrategy: HiddenItemStrategy.Ignore,
menuOptions: { shouldForwardArgs: true },
highlightToggledItems: true,
// Render every group in the primary slot with a separator between groups
// so the New Chat action sits visually separated from the pin/maximize/close cluster.
toolbarOptions: { primaryGroup: () => true, useSeparatorsInPrimaryActions: true },
}));

this._metaRow = $('.chat-composite-bar-meta-row');
Expand Down Expand Up @@ -140,12 +134,12 @@ export class SessionHeader extends Disposable {
return;
}

// Don't initiate a drag when the gesture starts inside one of the
// header toolbars (Run, Open in VS Code, New Chat). A small pointer
// Don't initiate a drag when the gesture starts inside the header
// toolbar (Run, Open in VS Code, New Chat, pin, close). A small pointer
// move during a button click would otherwise start a session drag
// and swallow the click.
const target = e.target as Node | null;
if (target && (this._titleActionsEl.contains(target) || this._inlineToolbarContainer.contains(target))) {
if (target && this._titleActionsEl.contains(target)) {
e.preventDefault();
return;
}
Expand Down Expand Up @@ -176,7 +170,6 @@ export class SessionHeader extends Disposable {
}
this._session = session;
this._toolbar.context = session;
this._inlineToolbar.context = session;

const store = new DisposableStore();
this._sessionDisposables.value = store;
Expand Down Expand Up @@ -267,10 +260,7 @@ export class SessionHeader extends Disposable {
hasMeta = true;
}

// The New Chat (`+`) toolbar always lives in the meta row (next to the diff
// stats), so the meta row is never empty.
this._metaRow.appendChild(this._inlineToolbarContainer);

this._metaRow.style.display = hasMeta ? '' : 'none';
this._onDidChangeHeight.fire();
}

Expand Down Expand Up @@ -345,6 +335,7 @@ export class SessionViewFloatingToolbar extends Disposable {
hiddenItemStrategy: HiddenItemStrategy.Ignore,
menuOptions: { shouldForwardArgs: true },
highlightToggledItems: true,
toolbarOptions: { primaryGroup: () => true, useSeparatorsInPrimaryActions: true },
}));

this._setVisible(false);
Expand Down
Loading
Loading