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: 3 additions & 2 deletions docs/convergence/protocol-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ This document is generated from [`protocols/registry.json`](../../protocols/regi
| [credential-provider-ledgers](#credential-provider-ledgers) | `internal-durable` | Credential, provider, model-route, and pricing maintainers | 24 | true |
| [desktop-risk-restart-session](#desktop-risk-restart-session) | `ephemeral` | Desktop shell lifecycle maintainers | 2 | false |
| [desktop-verification-settings](#desktop-verification-settings) | `internal-durable` | Desktop verification settings maintainers | 1 | true |
| [desktop-web-presentation-state](#desktop-web-presentation-state) | `projection` | Desktop and React workbench maintainers | 20 | true |
| [desktop-web-presentation-state](#desktop-web-presentation-state) | `projection` | Desktop and React workbench maintainers | 21 | true |
| [docker-attach-process-session](#docker-attach-process-session) | `ephemeral` | Docker runtime transport maintainers | 1 | false |
| [exported-evidence-and-handoff](#exported-evidence-and-handoff) | `external-durable` | Evidence, verification, report, and handoff maintainers | 35 | true |
| [extension-package-contracts](#extension-package-contracts) | `external-durable` | Skill, Plugin, Hook, and extension maintainers | 48 | true |
Expand Down Expand Up @@ -782,7 +782,7 @@ This document is generated from [`protocols/registry.json`](../../protocols/regi
- Readers:
- `desktop-web-presentation-state-reader` (`v1`, active) at `web/src`

<details><summary>20 active identifiers</summary>
<details><summary>21 active identifiers</summary>

- `desktop_chat_resource.v1`
- `desktop_clipboard_files.v1`
Expand All @@ -802,6 +802,7 @@ This document is generated from [`protocols/registry.json`](../../protocols/regi
- `prayu.run-navigation.v1`
- `universal-code.batch-workbench.v1`
- `universal-code.chat-preferences.v1`
- `universal-code.sidebar-width.v1`
- `universal-code.specialist-continuation.v1`
- `v2_file_upload.v1`

Expand Down
27 changes: 27 additions & 0 deletions docs/evidence/sidebar-width/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Sidebar width

The desktop sidebar edge can be dragged between 232 and 420 px. Its default remains 299 px. The current browser profile remembers the released width; a smaller window temporarily limits the sidebar to leave room for the conversation. Widening the window restores the preferred width. A click without a width change does not replace that preference.

The edge is keyboard accessible: Left/Right adjusts 12 px, Home/End selects the available minimum/maximum, and Enter or a double-click restores the default. Escape, pointer cancellation, losing pointer capture, or leaving the window cancels an unfinished drag. At 720 px and below, the existing overlay drawer retains its own width and the resize handle is unavailable.

## Actual browser verification

Windows Chrome, production Go API and SQLite, using the offline `workbenchjourney` provider. Both bundles displayed the same saved project and conversation. No screenshot content or styles were injected.

| Capture | Result |
| --- | --- |
| [Before](before-fixed.png) | Base `3dac0ecc`, 1440 × 1000; fixed 299 px sidebar truncates the sample names. |
| [After](after-expanded.png) | This change, 1440 × 1000; real pointer drag to 420 px reveals more of the same names. |
| [Dark theme](after-dark.png) | Existing dark theme selected through Settings, 420 px sidebar. |
| [Narrow screen](after-narrow.png) | 390 × 844; existing 286 px overlay, no resize handle. |

Observed interactions:

- A 121 px pointer move changed 299 px to 420 px during the drag and on release. The draft stayed intact.
- At an 800 px viewport, the sidebar displayed 380 px while retaining the saved 420 px. A click on the edge kept the stored preference; widening restored 420 px.
- Keyboard Left changed 420 to 408 px; Enter restored 299 px. End restored the available 420 px maximum.
- Navigating away and reconnecting restored both the saved width and the draft. Settings also uses the same sidebar width.
- The narrow drawer still opens and closes through its normal controls. Returning to a wide viewport restores the desktop width.
- A smaller conversation area exposed the existing user-message tooltip overflow already addressed separately by #441. This PR does not duplicate that patch.

68 related component, app and search tests passed. After fixing the no-drag preference case, the 5 resize tests passed again. Production build, protocol registration check against `3dac0ecc`, and `git diff --check` passed. The removed legacy resize component and CSS had no remaining consumers. Native packaged desktop and touch hardware were not tested.
Binary file added docs/evidence/sidebar-width/after-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/evidence/sidebar-width/after-expanded.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/evidence/sidebar-width/after-narrow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/evidence/sidebar-width/before-fixed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions protocols/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -1316,6 +1316,7 @@
"prayu.run-navigation.v1",
"universal-code.batch-workbench.v1",
"universal-code.chat-preferences.v1",
"universal-code.sidebar-width.v1",
"universal-code.specialist-continuation.v1",
"v2_file_upload.v1"
],
Expand Down
23 changes: 0 additions & 23 deletions web/src/components/workbench-frame.test.tsx

This file was deleted.

53 changes: 1 addition & 52 deletions web/src/components/workbench-frame.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,7 @@
import { useRef, type KeyboardEvent, type PointerEvent, type ReactNode } from "react";
import type { ReactNode } from "react";
import type { APIClient } from "../api/client";
import { useLocale } from "../lib/locale";
import { WorkbenchDock, type WorkbenchResourceKind } from "./workbench-dock";

export const minimumSidebarWidth = 232;
export const maximumSidebarWidth = 420;
export const defaultSidebarWidth = 286;

export function clampSidebarWidth(width: number): number {
return Math.min(maximumSidebarWidth, Math.max(minimumSidebarWidth, Math.round(width)));
}

export function SidebarResizeHandle({ value, onChange }: {
value: number;
onChange: (value: number) => void;
}) {
const { t } = useLocale();
const drag = useRef<{ pointerID: number; originX: number; originWidth: number } | null>(null);
const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
event.preventDefault();
onChange(clampSidebarWidth(value + (event.key === "ArrowLeft" ? -12 : 12)));
} else if (event.key === "Home") {
event.preventDefault();
onChange(minimumSidebarWidth);
} else if (event.key === "End") {
event.preventDefault();
onChange(maximumSidebarWidth);
}
};
const onPointerDown = (event: PointerEvent<HTMLDivElement>) => {
if (event.button !== 0) return;
drag.current = { pointerID: event.pointerId, originX: event.clientX, originWidth: value };
event.currentTarget.setPointerCapture?.(event.pointerId);
};
const onPointerMove = (event: PointerEvent<HTMLDivElement>) => {
if (!drag.current || drag.current.pointerID !== event.pointerId) return;
onChange(clampSidebarWidth(drag.current.originWidth + event.clientX - drag.current.originX));
};
const finishDrag = (event: PointerEvent<HTMLDivElement>) => {
if (!drag.current || drag.current.pointerID !== event.pointerId) return;
drag.current = null;
if (event.currentTarget.hasPointerCapture?.(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId);
}
};
return <div aria-label={t("调整侧栏宽度", "Resize sidebar")} aria-orientation="vertical"
aria-valuemax={maximumSidebarWidth} aria-valuemin={minimumSidebarWidth}
aria-valuenow={value} className="sidebar-resize-handle"
onDoubleClick={() => onChange(defaultSidebarWidth)} onKeyDown={onKeyDown}
onPointerCancel={finishDrag} onPointerDown={onPointerDown}
onPointerMove={onPointerMove} onPointerUp={finishDrag} role="separator" tabIndex={0} />;
}

export function WorkbenchFrame({ title, children, client, desktop, resourceKind, runID,
sessionID, threadID = "" }: {
title: string;
Expand Down
30 changes: 0 additions & 30 deletions web/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1664,16 +1664,6 @@ body { background: transparent; }
grid-template-columns: minmax(0, 1fr);
}

.sidebar-resize-handle {
position: relative;
z-index: 20;
width: 16px;
cursor: col-resize;
touch-action: none;
user-select: none;
}
.sidebar-resize-handle:focus-visible { outline: 0; }

.prayu-shell .main-workspace.prayu-conversation-panel {
min-width: 0;
min-height: 0;
Expand Down Expand Up @@ -2497,7 +2487,6 @@ body { background: transparent; }
display: grid;
grid-template-columns: minmax(0, 1fr);
}
.sidebar-resize-handle { display: none; }
.prayu-shell .main-workspace.prayu-conversation-panel {
height: 100%;
min-height: 0;
Expand Down Expand Up @@ -2929,25 +2918,6 @@ summary:focus-visible {
background: var(--prayu-control-hover);
}

.sidebar-resize-handle {
background: transparent;
}

.sidebar-resize-handle::after {
position: absolute;
inset: 14px 7px;
width: 2px;
border-radius: 1px;
background: transparent;
content: "";
transition: background 120ms ease;
}

.sidebar-resize-handle:hover::after,
.sidebar-resize-handle:focus-visible::after {
background: var(--prayu-border-strong);
}

.about-mark {
border-color: var(--prayu-border-strong);
color: var(--prayu-text);
Expand Down
7 changes: 7 additions & 0 deletions web/src/v2/app.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -270,13 +270,20 @@ describe("V2Workbench inspector navigation", () => {
expect(screen.getByRole("navigation", { name: "对话导航" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "隐藏侧栏" })).toHaveAttribute("aria-pressed", "true");

const resizeHandle = screen.getByRole("separator", { name: "调整侧栏宽度" });
resizeHandle.focus();
fireEvent.keyDown(resizeHandle, { key: "ArrowRight" });
expect(resizeHandle).toHaveAttribute("aria-valuenow", "311");
act(() => { narrowListener?.({ matches: true } as MediaQueryListEvent); });
expect(screen.queryByRole("navigation", { name: "对话导航" })).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "显示侧栏" })).toHaveAttribute("aria-pressed", "false");
expect(screen.getByRole("button", { name: "显示侧栏" })).toHaveFocus();
expect(screen.queryByRole("separator", { name: "调整侧栏宽度" })).not.toBeInTheDocument();

act(() => { narrowListener?.({ matches: false } as MediaQueryListEvent); });
expect(screen.getByRole("navigation", { name: "对话导航" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "隐藏侧栏" })).toHaveAttribute("aria-pressed", "true");
expect(screen.getByRole("separator", { name: "调整侧栏宽度" })).toHaveAttribute("aria-valuenow", "311");

screen.getByRole("button", { name: "新对话" }).focus();
act(() => { narrowListener?.({ matches: true } as MediaQueryListEvent); });
Expand Down
11 changes: 5 additions & 6 deletions web/src/v2/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { V2ConfirmDialog } from "./components/dialog";
import { V2LazySurface } from "./components/lazy-surface";
import { V2NetworkScopeControl, type V2NetworkMode } from "./components/network-scope-control";
import { V2SettingsSidebar, V2Sidebar, type V2SettingsSection } from "./components/sidebar";
import { V2SidebarLayout } from "./components/sidebar-layout";
import { V2Titlebar } from "./components/titlebar";
import { useV2Client } from "./client-session";
import { v2QueryKeys } from "./query-keys";
Expand Down Expand Up @@ -633,9 +634,8 @@ function V2WorkbenchContent({ client }: { client: APIClient }) {
onToggleSidebar={() => setSidebarVisible(!sidebarVisible)} sidebarVisible={sidebarVisible} sidebarToggleRef={sidebarToggle} />
<button aria-label={t("关闭侧栏", "Close sidebar")} className="v2-sidebar-backdrop" aria-hidden={!sidebarVisible} inert={!sidebarVisible}
onClick={() => setSidebarVisible(false)} type="button" />
<div className="v2-shell-body">
<div className="v2-sidebar-slot" ref={sidebarSlot} aria-hidden={!sidebarVisible} inert={!sidebarVisible}>
{surface === "settings" ? <V2SettingsSidebar onBack={returnFromSettings}
<V2SidebarLayout sidebarRef={sidebarSlot} visible={sidebarVisible} narrow={narrowLayout}
sidebar={surface === "settings" ? <V2SettingsSidebar onBack={returnFromSettings}
onSelect={setSettingsSection} section={settingsSection} /> : <V2Sidebar
onArchive={setArchiveCandidate} onNewConversation={() => startNew()} onOpenModels={() => openModels(newConversation)}
onNewInProject={(id) => startNew(id)} client={client}
Expand Down Expand Up @@ -675,8 +675,7 @@ function V2WorkbenchContent({ client }: { client: APIClient }) {
refreshing={sidebarQuery.isFetching && !sidebarQuery.isLoading && !sidebarQuery.isFetchingNextPage}
onRetry={() => void (sidebarQuery.isFetchNextPageError ? sidebarQuery.fetchNextPage() : sidebarQuery.refetch())}
onLoadMore={() => void sidebarQuery.fetchNextPage()} onRefresh={() => void sidebarQuery.refetch()}
selectedThreadID={newConversation ? "" : selectedThreadID} threads={threads} workspaces={workspaces} />}
</div>
selectedThreadID={newConversation ? "" : selectedThreadID} threads={threads} workspaces={workspaces} />}>
{editingProject && <ProjectDialog client={client} workspace={editingProject} onClose={() => setEditingProject(null)}
onSaved={(workspace) => setImportedWorkspaces((current) => [...current.filter(({ id }) => id !== workspace.id), workspace])} />}
<div className="v2-product-surface" inert={moveMutation.isPending && moveMutation.variables.thread.id === selectedThreadID}
Expand Down Expand Up @@ -813,7 +812,7 @@ function V2WorkbenchContent({ client }: { client: APIClient }) {
messageID={route.messageID} onExitMessage={() => navigate({ kind: "thread", threadID: selectedThreadID })}
threadID={selectedThreadID} workspaces={workspaces} />}
</div>
</div>
</V2SidebarLayout>
{backgroundCreations.length > 0 && <div className="v2-toast" role="status">
{backgroundCreations.map((thread) => <div key={thread.id}>
{t("对话已创建:", "Conversation created: ")}{thread.title}
Expand Down
12 changes: 12 additions & 0 deletions web/src/v2/components/sidebar-layout.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.v2-sidebar-layout > .v2-sidebar-slot { position: relative; }
.v2-sidebar-layout[data-resizing="true"] { transition: none; cursor: col-resize; user-select: none; }
.v2-sidebar-resize-handle { position: absolute; z-index: 30; top: 0; right: 0; bottom: 0; width: 8px; cursor: col-resize; touch-action: none; user-select: none; }
.v2-sidebar-resize-handle[hidden] { display: none; }
.v2-shell .v2-sidebar-resize-handle:focus-visible { outline: none; }
.v2-sidebar-resize-handle::after { content: ""; position: absolute; inset: 10px 3px; border-radius: 2px; background: transparent; transition: background 120ms ease; }
.v2-sidebar-resize-handle:hover::after,
.v2-sidebar-resize-handle:focus-visible::after,
.v2-sidebar-layout[data-resizing="true"] .v2-sidebar-resize-handle::after { background: var(--v2-accent); }
@media (max-width: 720px) {
.v2-sidebar-layout > .v2-sidebar-slot { position: absolute; }
}
Loading
Loading