feat(web): session list one-list, hover card, row menus, rename, manual ordering
Sidebar session list grows the figma 239-10458 feature set and the
workspace/session browsing region moves wholesale into ui-workspace:
- Group-by menu (WorkSpace / In one list): flat mode lists every session
top-level, strictly newest-first; the choice persists across reloads.
- Session rows get a 500ms hover detail card (title / relative time /
status line) and a ... menu (Rename / Fork session / Delete session,
visual-only for now); workspace headers get ... with Rename (wired) and
Delete workspace (visual-only).
- workspace.rename RPC: trims, rejects duplicate titles on the create
chain (workspace-name-conflict), no-op on same title; modal dialog with
client-side duplicate pre-check.
- workspace.insertSessionBefore RPC (DOM-insertBefore semantics, omitted
anchor appends): HTML5 drag reorder of root sessions inside a workspace
group; order truth stays host-side, the view refreshes from the
response/changed frame.
- Activity pinning removed: the session/event touchSession chain is gone;
workspace accounts are manually owned (new sessions prepend, explicit
reordering only). Contracts and tests updated, api catalog regenerated.
- ui-sidebar reduced to the column shell (brand, fold state machine, New
Session, Settings) exposing one sidebar.workspaces hole with a two-fact
owner share {wide, expandSidebar}; ui-workspace owns the whole region
(header, search, grouped/flat lists, dialogs, drag) plus the picker via
a shared WorkspaceCreateFlow. The old sidebar.workspace picker slot and
its deferral indirection are gone.
- ui-primitives: Menu gains label entries, danger rows, and
closeOnPointerLeave; new HoverCard (portaled, open-delay, disabled
guard). Hover card and row menu never coexist.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/** Workspace baseline, incremental-frame, and unary-action owner. */
|
||||
|
||||
import type {
|
||||
HostFrame, IApiClient, RpcError, RpcRequest, RpcResult, WorkspaceView,
|
||||
HostFrame, IApiClient, RpcError, RpcRequest, RpcResult, SessionId, WorkspaceId, WorkspaceView,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { transportError } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
import { mergeOrderedBaseline } from '../ordered-baseline.ts'
|
||||
@@ -143,6 +143,40 @@ export class WorkspaceManager {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a Workspace, then publish its returned snapshot without waiting
|
||||
* for the changed frame.
|
||||
* @param workspaceId - target workspace.
|
||||
* @param title - new display title.
|
||||
* @returns the wire result.
|
||||
*/
|
||||
async rename(workspaceId: WorkspaceId, title: string): Promise<RpcResult<{ workspace: WorkspaceView }>> {
|
||||
const { result } = await this.api.workspace.rename({ workspaceId, title })
|
||||
if (result.ok) this.upsert(result.value.workspace)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a session within its Workspace's manual order, then publish the
|
||||
* returned snapshot without waiting for the changed frame.
|
||||
* @param workspaceId - owning workspace.
|
||||
* @param sessionId - accounted session to move.
|
||||
* @param beforeSessionId - accounted anchor to insert before; omitted appends.
|
||||
* @returns the wire result.
|
||||
*/
|
||||
async insertSessionBefore(
|
||||
workspaceId: WorkspaceId,
|
||||
sessionId: SessionId,
|
||||
beforeSessionId?: SessionId,
|
||||
): Promise<RpcResult<{ workspace: WorkspaceView }>> {
|
||||
const { result } = await this.api.workspace.insertSessionBefore({
|
||||
workspaceId, sessionId,
|
||||
...beforeSessionId === undefined ? {} : { beforeSessionId },
|
||||
})
|
||||
if (result.ok) this.upsert(result.value.workspace)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Host-frame entry. Non-workspace frames are ignored so the runtime can
|
||||
* fan one host stream out to both object managers.
|
||||
@@ -189,6 +223,11 @@ export class WorkspaceManager {
|
||||
private upsert(view: WorkspaceView, identity?: Workspace): void {
|
||||
this.refreshFrames?.push(view)
|
||||
const index = this.items.findIndex(item => item.getSnapshot().view?.workspaceId === view.workspaceId)
|
||||
// Mutation responses and changed frames race (two carriers, no ordering):
|
||||
// reject a snapshot strictly older than the installed projection so a
|
||||
// late unary response cannot roll back a newer frame.
|
||||
const installed = index === -1 ? undefined : this.items[index]?.getSnapshot().view
|
||||
if (installed !== undefined && Date.parse(view.updatedAt) < Date.parse(installed.updatedAt)) return
|
||||
if (identity !== undefined) {
|
||||
this.items = index === -1
|
||||
? [identity, ...this.items]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import type {
|
||||
IApiClient, RpcError, WorkspaceId, WorkspaceView,
|
||||
IApiClient, RpcError, SessionId, WorkspaceId, WorkspaceView,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SnapshotStore } from '../contract/store.ts'
|
||||
import { createSnapshotStore } from '../contract/store.ts'
|
||||
@@ -100,6 +100,35 @@ export class WorkspacesService {
|
||||
return result.value.workspace
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a Workspace.
|
||||
* @param workspaceId - target workspace.
|
||||
* @param title - new display title (trimmed non-empty by the Host).
|
||||
* @returns the renamed Workspace view.
|
||||
*/
|
||||
async rename(workspaceId: WorkspaceId, title: string): Promise<WorkspaceView> {
|
||||
const result = await this.manager.rename(workspaceId, title)
|
||||
if (!result.ok) throw new Error(`workspace rename failed: ${result.error.code}: ${result.error.message}`)
|
||||
return result.value.workspace
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a session within its Workspace's manual order (DOM-insertBefore-like).
|
||||
* @param workspaceId - owning workspace.
|
||||
* @param sessionId - accounted session to move.
|
||||
* @param beforeSessionId - accounted anchor to insert before; omitted appends.
|
||||
* @returns the updated Workspace view.
|
||||
*/
|
||||
async insertSessionBefore(
|
||||
workspaceId: WorkspaceId,
|
||||
sessionId: SessionId,
|
||||
beforeSessionId?: SessionId,
|
||||
): Promise<WorkspaceView> {
|
||||
const result = await this.manager.insertSessionBefore(workspaceId, sessionId, beforeSessionId)
|
||||
if (!result.ok) throw new Error(`workspace move failed: ${result.error.code}: ${result.error.message}`)
|
||||
return result.value.workspace
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the workspace baseline, reusing an in-flight pull.
|
||||
* @returns completion of the current or newly started workspace baseline pull.
|
||||
|
||||
@@ -92,9 +92,18 @@ export class FakeApiClient implements IApiClient {
|
||||
onWorkspaceCreate: (payload: unknown) => Promise<RpcResponse<{ workspace: WorkspaceView; created: boolean }>> =
|
||||
() => Promise.resolve(ok({ workspace: fakeWorkspace('fk-ws'), created: true }))
|
||||
|
||||
onWorkspaceRename: (payload: unknown) => Promise<RpcResponse<{ workspace: WorkspaceView }>> =
|
||||
() => Promise.resolve(ok({ workspace: fakeWorkspace('fk-ws') }))
|
||||
|
||||
onWorkspaceInsertSessionBefore: (payload: unknown) => Promise<RpcResponse<{ workspace: WorkspaceView }>> =
|
||||
() => Promise.resolve(ok({ workspace: fakeWorkspace('fk-ws') }))
|
||||
|
||||
readonly workspace: IApiClient['workspace'] = {
|
||||
list: (payload: unknown) => this.record('workspace.list', payload, this.onWorkspaceList(payload)),
|
||||
create: (payload: unknown) => this.record('workspace.create', payload, this.onWorkspaceCreate(payload)),
|
||||
rename: (payload: unknown) => this.record('workspace.rename', payload, this.onWorkspaceRename(payload)),
|
||||
insertSessionBefore: (payload: unknown) =>
|
||||
this.record('workspace.insertSessionBefore', payload, this.onWorkspaceInsertSessionBefore(payload)),
|
||||
}
|
||||
|
||||
/** When true, streams never fire onOpen (misbehaving-carrier material for the handshake timeout guard). */
|
||||
|
||||
Reference in New Issue
Block a user