Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7c2585d
feat(web-shell): improve sidebar session management
Aug 14, 2026
a9e32d5
Merge branch 'main' into codex/web-shell-sidebar-session-details
wenshao Aug 14, 2026
e06097a
Merge branch 'main' into codex/web-shell-sidebar-session-details
qwen-code-dev-bot Aug 14, 2026
764c076
feat(web-shell): keep sessions accessible when sidebar is collapsed
Aug 14, 2026
2093af6
Merge remote-tracking branch 'origin/codex/web-shell-sidebar-session-…
Aug 14, 2026
5816dd6
test(web-shell): cover hover session details
Aug 14, 2026
0357d6b
test(web-shell): align workspace sidebar coverage
Aug 14, 2026
4127cf0
fix(web-shell): keep collapsed session actions open
Aug 14, 2026
1af1690
fix(web-shell): layer collapsed session menus
Aug 14, 2026
4b02211
Merge branch 'main' into codex/web-shell-sidebar-session-details
qwen-code-dev-bot Aug 14, 2026
80b97ad
Merge branch 'main' into codex/web-shell-sidebar-session-details
qwen-code-dev-bot Aug 14, 2026
71b6dc4
Merge branch 'main' into codex/web-shell-sidebar-session-details
qwen-code-dev-bot Aug 14, 2026
b3497b1
fix(web-shell): address sidebar session details review findings (#9122)
qwen-code-dev-bot Aug 14, 2026
9d6f0eb
fix(web-shell): resolve standing sidebar session details blockers (#9…
qwen-code-dev-bot Aug 15, 2026
9cfd169
fix: address round-6 review findings in serve metadata and sidebar (#…
qwen-code-dev-bot Aug 15, 2026
52fca13
fix: address round-7 review findings in serve metadata and sidebar (#…
qwen-code-dev-bot Aug 15, 2026
9a8e8a6
fix: address round-8 review findings in sidebar rename and switcher (…
qwen-code-dev-bot Aug 15, 2026
84e5b76
fix: address round-9 review findings in sidebar rename and menus (#9122)
qwen-code-dev-bot Aug 15, 2026
cab964f
fix: address round-10 review findings in sidebar actions and rename (…
qwen-code-dev-bot Aug 16, 2026
2fee896
Merge branch 'main' into codex/web-shell-sidebar-session-details
qwen-code-dev-bot Aug 16, 2026
ebed5d9
Merge branch 'main' into codex/web-shell-sidebar-session-details
qwen-code-dev-bot Aug 16, 2026
ccf1f68
Merge branch 'main' into codex/web-shell-sidebar-session-details
qwen-code-dev-bot Aug 16, 2026
5d6bc65
Merge branch 'main' into codex/web-shell-sidebar-session-details
qwen-code-dev-bot Aug 16, 2026
b8825e7
Merge remote-tracking branch 'origin/main' into codex/web-shell-sideb…
qwen-code-dev-bot Aug 16, 2026
6eff160
Merge branch 'main' into codex/web-shell-sidebar-session-details
qwen-code-dev-bot Aug 17, 2026
97f3fca
fix(web-shell): remove unsafe rename unmount cleanup
Aug 17, 2026
e854130
fix(web-shell): stabilize sidebar session mutations
Aug 17, 2026
30b74a4
fix(web-shell): polish sidebar session interactions
Aug 17, 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
41 changes: 41 additions & 0 deletions docs/design/2026-08-13-web-shell-sidebar-session-details.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Web Shell sidebar session details

## Goal

Make session rows easier to scan without adding another navigation surface:

- show the existing details panel from row hover and remove the Details action
from the overflow menu;
- preview five sessions per expanded folder or session group, with an explicit
control to reveal the remainder until that section is collapsed;
- move timestamps into the details panel and reserve the row's trailing slot
for branch or worktree state;
- fade overflowing titles at the right edge and scroll them slowly on hover;
- use a neutral spinner for running sessions;
- keep the brand, New task action, and footer fixed while the remaining
navigation and session content share one scroll area.

## Design

The row remains the only session-selection and keyboard target. A controlled
Radix popover is anchored to it and opens only from pointer hover. The panel
does not participate in keyboard navigation; its session ID copy action is a
pointer-only affordance. The panel contains the title and relative time, final
workspace path segment, optional git branch or worktree, session status, and a
copyable session ID. Existing action menus keep all mutation actions but no
longer include Details. Rename targets the selected session through its owning
workspace, so current, background, secondary-workspace, and archived sessions
share the same action.

Session limits are local UI state. Direct workspace lists and grouped lists
show the first five items; revealing the remainder is not persisted, so
collapsing and reopening the owning section restores the five-item preview.

Title overflow uses a CSS mask for the trailing fade. On hover, one DOM width
measurement supplies the exact scroll distance to a CSS animation, avoiding a
timer or dependency.

The workspace-qualified metadata route keeps background-session renames inside
the resolved workspace runtime. Its dedicated `workspace_session_metadata`
capability prevents clients from exposing the action against older daemons
that do not mount the route. No session schema changes are required.
15 changes: 15 additions & 0 deletions docs/design/2026-08-14-web-shell-collapsed-session-switcher.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Web Shell collapsed session switcher

## Goal

Keep session switching available while the sidebar is collapsed without adding
another navigation model.

## Design

The collapsed sidebar shows one Project icon in the scrolling navigation area.
Pointer hover or click opens a Popover containing the same complete session
browser used by the expanded sidebar. Source tabs, pinned and live sessions,
project search, workspace actions, grouping, preview limits, archived sessions,
and expansion preferences therefore follow one implementation in both states.
Selecting a session closes the Popover.
1 change: 1 addition & 0 deletions integration-tests/cli/qwen-serve-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@ describe('qwen serve — capabilities envelope', () => {
'workspace_persisted_transcript',
'workspace_session_export',
'workspace_archived_session_export',
'workspace_session_metadata',
Comment thread
ytahdn marked this conversation as resolved.
'voice_transcribe',
Comment thread
ytahdn marked this conversation as resolved.
]);
});
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/serve/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,9 @@ export const SERVE_CAPABILITY_REGISTRY = {
// This remains independent from active export so older daemons cannot ignore
// archive intent and return an active transcript with the same session id.
workspace_archived_session_export: { since: 'v1' },
// Workspace-qualified metadata updates for active, inactive, and archived
// persisted sessions.
workspace_session_metadata: { since: 'v1' },
// Workspace-qualified ACP transport (issue #6378 Phase 4):
// `/workspaces/:workspace/acp` mounts a per-runtime ACP dispatcher (HTTP +
// WebSocket) for each registered workspace, with per-runtime device-flow and
Expand Down
119 changes: 119 additions & 0 deletions packages/cli/src/serve/routes/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { parseChannelDelivery } from '../../runtime/channel-delivery.js';
import {
canonicalizeWorkspace,
InvalidClientIdError,
InvalidSessionMetadataError,
PromptQueueFullError,
SessionArtifactValidationError,
SessionArchivedError,
Expand Down Expand Up @@ -4844,6 +4845,124 @@ export function registerSessionRoutes(
),
);

app.patch(
'/workspaces/:workspace/session/:id/metadata',
mutate({ strict: true }),
Comment thread
ytahdn marked this conversation as resolved.
async (req, res) => {
const route = 'PATCH /workspaces/:workspace/session/:id/metadata';
const runtime = requireTrustedRuntimeForWorkspaceRoute(req, res, route);
if (!runtime) return;
const sessionId = requireSessionId(req, res);
if (sessionId === null) return;
const clientId = parseClientIdHeader(req, res);
if (clientId === null) return;
const rawDisplayName = safeBody(req)['displayName'];
if (typeof rawDisplayName !== 'string') {
res.status(400).json({
error: '`displayName` must be a string',
code: 'invalid_metadata',
field: 'displayName',
});
return;
}
try {
const displayName = rawDisplayName.slice(0, 256);
Comment thread
ytahdn marked this conversation as resolved.
Comment thread
ytahdn marked this conversation as resolved.
if (displayName.trim() === '') {
Comment thread
ytahdn marked this conversation as resolved.
// An empty name would append an empty custom_title record to
// persisted sessions, which the title readers disagree on.
throw new InvalidSessionMetadataError(
'displayName',
'must not be empty',
);
}
if (
Array.from(displayName).some((character) => {
const code = character.charCodeAt(0);
return code <= 31 || code === 127;
Comment thread
ytahdn marked this conversation as resolved.
Comment thread
ytahdn marked this conversation as resolved.
})
) {
throw new InvalidSessionMetadataError(
'displayName',
'must not contain control characters',
);
}
await archiveCoordinator.runExclusiveMany([sessionId], async () => {
const assertRuntimeGenerationOpen =
captureRuntimeGenerationAssertion(runtime);
assertRuntimeGenerationOpen?.();
const liveOwner =
workspaceRegistry.resolveLiveSessionOwner(sessionId);
if (liveOwner.kind === 'unavailable') {
sendWorkspaceRuntimeUnavailable(res);
return;
}
if (liveOwner.kind === 'ambiguous') {
sendAmbiguousSessionOwner(
res,
route,
sessionId,
liveOwner.runtimes,
);
return;
}
if (
liveOwner.kind === 'found' &&
liveOwner.runtime.workspaceCwd !== runtime.workspaceCwd
) {
sendSessionWorkspaceConflict(
res,
route,
sessionId,
runtime,
liveOwner.runtime,
);
return;
}
await runWithWorkspaceRuntimeStorage(runtime, async () => {
let effective: { displayName?: string };
Comment thread
ytahdn marked this conversation as resolved.
try {
effective = runtime.bridge.updateSessionMetadata(
sessionId,
{ displayName },
clientId !== undefined ? { clientId } : undefined,
);
assertRuntimeGenerationOpen?.();
} catch (err) {
if (!(err instanceof SessionNotFoundError)) throw err;
const service = createWorkspaceRuntimeSessionService(runtime);
const location = await service.getSessionLocation(sessionId);
Comment thread
ytahdn marked this conversation as resolved.
assertRuntimeGenerationOpen?.();
if (location === 'conflict') {
throw new SessionConflictError(sessionId);
}
const renamed = location
? await service.renameSession(
sessionId,
displayName,
'manual',
location,
)
: false;
assertRuntimeGenerationOpen?.();
if (!renamed) {
throw new SessionNotFoundError(sessionId);
}
effective = { displayName: displayName || undefined };
}
invalidateSessionLists(runtime, ['active', 'archived']);
res.status(200).json({ sessionId, ...effective });
});
});
} catch (err) {
sendBridgeError(res, err, {
route,
sessionId,
workspaceCwd: runtime.workspaceCwd,
});
}
},
);

type SessionOrganizationTarget = {
runtime?: WorkspaceRuntime;
resolveRuntime?: (
Expand Down
Loading
Loading