feat(app): pin chats to the top of the sidebar (#1981)

* feat(app): pin projects to top of the sidebar

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(app): dedicated Pinned sidebar section with UX fixes

Replaces the invisible float-in-place pinning with a Codex-style Pinned
section at the top of the sidebar that hoists pinned chats and projects.

- Pin-aware keyboard shortcut numbering (badges match visual order)
- Pinned chats show their project name as subtitle (context after hoisting)
- Pinned entries ordered most-recently-pinned first
- Collapsible Pinned section (persisted)
- Double-click guard on workspace pin toggle

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(app): move Pinned section above the Workspaces header

Pinned now sits at the top of the scroll, above the "Workspaces" section
header (which moves into the scroll below it), matching the market-standard
sidebar layout. Chevron sits beside the "Pinned" label.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(app): reveal Pinned collapse chevron on hover only

Extracts PinnedSectionHeader with its own hover state; the chevron hides
until hover on web and stays visible on touch/compact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(app): pin the active chat via keyboard shortcut and inline pin button

Adds a ⌘⇧P / Ctrl+Shift+P shortcut that pins the selected chat, mirroring
the worktree.archive action across actions/dispatcher/route-shortcut, gated
by `selected && supportsWorkspacePinning`. Also surfaces an inline pin button
on row hover (always-on for touch) alongside the kebab in the shared trailing
overlay, swapping Pin/PinOff by pinned state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): unify project and workspace pin handlers

Collapses the two near-identical pin.set handlers into a shared
handlePinSetRequest that owns control flow, error handling and the
workspace-update fan-out; each caller only supplies its registry apply
step and typed response payload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(app): drop empty pinned project shells and empty Workspaces header

Pinning every chat of a project hoisted them into Pinned but left an empty
project header (plus its new-workspace ghost row) below, duplicating the
project. Skip projects emptied by pinning while keeping genuinely empty
projects. Also hide the "Workspaces" section header when no unpinned projects
remain, so it no longer floats above nothing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(protocol): correct workspacePinning COMPAT version to v0.1.105

Feature ships in 0.1.105 per the changelog, not the placeholder v0.1.103.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(app): hide redundant chat pin control inside pinned projects

A pinned project already hoists all its chats to the top, so the per-chat pin
was a no-op that just toggled a dead icon. Suppress the inline pin button, the
kebab pin item, and the pin keyboard shortcut on chats whose project is pinned;
they still work for chats in unpinned projects.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(app): hide project pin control on empty projects

Project pin state is derived from workspace descriptors, so a project with no
workspaces would set pinnedAt on the server but emit no descriptor update — the
pin silently never surfaced and the user got no feedback. Offer the pin only
once the project has a chat, matching how chat pins are suppressed inside
already-pinned projects.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(app): drop collapsed Pinned rows from keyboard shortcut numbering

When the Pinned section is collapsed its chats and projects are hidden, but the
shortcut model kept counting them: visible unpinned rows showed off-by-N badge
numbers and pressing a number key jumped to a hidden pinned chat. Exclude the
pinned rows from numbering when Pinned is collapsed, matching what renders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(app): hydrate pinned sidebar rows

* fix(app): keep filtered workspace controls reachable

* feat(app): scope sidebar pinning to chats, drop project pinning

Per PR review: pinning whole projects overlapped with project reordering
and mixed projects into the pinned list. Keep chat pinning only.

- Remove the project.pin.set RPC, its server handler, the projectPinnedAt
  descriptor field, and the client setProjectPinned method.
- Drop the project pin control, isProjectPinned reads, and the suppressPin
  plumbing (its only source — a pinned project — is gone).
- Fold pin-aware shortcut ordering into SidebarModelProvider (the live
  shortcut owner) and delete the orphaned use-sidebar-shortcut-model hook,
  so pinned chats and their shortcut numbers share one projection.
- Prune the now-unused project pin i18n keys across all locales.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(server): inline the pin handler into the workspace path

With project pinning gone, handlePinSetRequest had a single caller. Fold it
back into handleWorkspacePinSetRequest and drop the project/workspace
generalization.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* revert: keep main's use-sidebar-shortcut-model untouched

This hook (and its test) already live on main. The scope-down had deleted
them, but that's out of scope for this PR — the pin-aware shortcut ordering
lives in SidebarModelProvider now, so main's hook is left exactly as it was.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(protocol): align workspacePinning COMPAT version in websocket-server

Match the v0.1.105 version already corrected in messages.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(app): reshape pinned sidebar updates

* chore(app): remove stale pin button styles

* refactor(app): simplify pinned workspace rows

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
rafavncxs
2026-07-13 05:59:02 -03:00
committed by GitHub
parent 8cd83b3a91
commit a1e81685a1
40 changed files with 1370 additions and 130 deletions

View File

@@ -831,6 +831,13 @@ export const WorkspaceTitleSetRequestSchema = z.object({
requestId: z.string(),
});
export const WorkspacePinSetRequestSchema = z.object({
type: z.literal("workspace.pin.set.request"),
workspaceId: z.string(),
pinned: z.boolean(),
requestId: z.string(),
});
export const SetVoiceModeMessageSchema = z.object({
type: z.literal("set_voice_mode"),
enabled: z.boolean(),
@@ -1450,6 +1457,19 @@ export const WorkspaceTitleSetResponseSchema = z.object({
payload: WorkspaceTitleSetResponsePayloadSchema,
});
export const WorkspacePinSetResponsePayloadSchema = z.object({
requestId: z.string(),
workspaceId: z.string(),
accepted: z.boolean(),
pinnedAt: z.string().nullable(),
error: z.string().nullable(),
});
export const WorkspacePinSetResponseSchema = z.object({
type: z.literal("workspace.pin.set.response"),
payload: WorkspacePinSetResponsePayloadSchema,
});
export const SetVoiceModeResponseMessageSchema = z.object({
type: z.literal("set_voice_mode_response"),
payload: z.object({
@@ -2084,6 +2104,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
ProjectRenameRequestSchema,
ProjectRemoveRequestSchema,
WorkspaceTitleSetRequestSchema,
WorkspacePinSetRequestSchema,
SetVoiceModeMessageSchema,
SendAgentMessageRequestSchema,
WaitForFinishRequestSchema,
@@ -2397,6 +2418,8 @@ export const ServerInfoStatusPayloadSchema = z
agentForkContext: z.boolean().optional(),
// COMPAT(providerSubagents): added in v0.1.107, remove gate after 2027-01-12.
providerSubagents: z.boolean().optional(),
// COMPAT(workspacePinning): added in v0.1.107, remove gate after 2027-01-12.
workspacePinning: z.boolean().optional(),
})
.optional(),
})
@@ -2675,6 +2698,8 @@ export const WorkspaceDescriptorPayloadSchema = z
// its input and offer a "reset to branch name" action. Null means the name
// is derived from the branch/directory.
title: z.string().nullable().optional(),
// COMPAT(workspacePinning): added in v0.1.107, remove optional after 2027-01-12.
pinnedAt: z.string().nullable().optional(),
archivingAt: z.string().nullable().optional().default(null),
status: WorkspaceStateBucketSchema,
// Best-effort workspace status entry timestamp. Old daemons omit the
@@ -4346,6 +4371,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
ProjectRenameResponseSchema,
ProjectRemoveResponseSchema,
WorkspaceTitleSetResponseSchema,
WorkspacePinSetResponseSchema,
WaitForFinishResponseMessageSchema,
AgentPermissionRequestMessageSchema,
AgentPermissionResolvedMessageSchema,
@@ -4502,6 +4528,8 @@ export type WorkspaceTitleSetResponse = z.infer<typeof WorkspaceTitleSetResponse
export type WorkspaceTitleSetResponsePayload = z.infer<
typeof WorkspaceTitleSetResponsePayloadSchema
>;
export type WorkspacePinSetResponse = z.infer<typeof WorkspacePinSetResponseSchema>;
export type WorkspacePinSetResponsePayload = z.infer<typeof WorkspacePinSetResponsePayloadSchema>;
export type WorkspaceCreateRequest = z.infer<typeof WorkspaceCreateRequestSchema>;
export type WorkspaceCreateResponse = z.infer<typeof WorkspaceCreateResponseSchema>;
export type ProjectRenameResponsePayload = z.infer<typeof ProjectRenameResponsePayloadSchema>;
@@ -4634,6 +4662,7 @@ export type UpdateAgentRequestMessage = z.infer<typeof UpdateAgentRequestMessage
export type ProjectRenameRequest = z.infer<typeof ProjectRenameRequestSchema>;
export type ProjectRemoveRequest = z.infer<typeof ProjectRemoveRequestSchema>;
export type WorkspaceTitleSetRequest = z.infer<typeof WorkspaceTitleSetRequestSchema>;
export type WorkspacePinSetRequest = z.infer<typeof WorkspacePinSetRequestSchema>;
export type SetAgentModeRequestMessage = z.infer<typeof SetAgentModeRequestMessageSchema>;
export type SetAgentModelRequestMessage = z.infer<typeof SetAgentModelRequestMessageSchema>;
export type SetAgentThinkingRequestMessage = z.infer<typeof SetAgentThinkingRequestMessageSchema>;