mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
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:
@@ -14,6 +14,8 @@ import type {} from '@deepseek-ai/dsh-session-persistence'
|
||||
import type { DomainGlobal, KvTable } from '@deepseek-ai/dsh-storage-domain'
|
||||
import { WorkspaceEntity } from './entity.ts'
|
||||
import type { WorkspaceEntityHost } from './entity.ts'
|
||||
|
||||
export { WorkspaceMoveInvalidError } from './entity.ts'
|
||||
import { realpathNormalize } from './paths.ts'
|
||||
import { workspaceDomainSpec } from './spec.ts'
|
||||
import type { WorkspaceDomainState, WorkspaceRecord } from './spec.ts'
|
||||
@@ -47,6 +49,7 @@ export class WorkspaceNameConflictError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
workspace: WorkspaceRegistry
|
||||
@@ -82,7 +85,6 @@ export class WorkspaceRegistry extends Service {
|
||||
private readonly headers = new Map<SessionId, SessionHeader>()
|
||||
private readonly sessionPaths = new Map<SessionId, string>()
|
||||
private readonly invalidSessionPaths = new Map<SessionId, string>()
|
||||
private readonly pendingTouches = new Map<SessionId, Promise<void>>()
|
||||
private operationTail: Promise<void> = Promise.resolve()
|
||||
|
||||
private readonly host: WorkspaceEntityHost = {
|
||||
@@ -120,13 +122,6 @@ export class WorkspaceRegistry extends Service {
|
||||
this.validateStoredState(this.requireState())
|
||||
this.rebuildEntities()
|
||||
this.reportFilteredCandidates()
|
||||
// Session activity is authoritative even when no RPC/SSE consumer is
|
||||
// connected. This service-owned listener is disposed with the registry.
|
||||
this.ctx.on('session/event', (session) => {
|
||||
void this.touchSession(session.id).catch((error: unknown) => {
|
||||
this.ctx.logger.warn(`workspace activity touch failed for session '${session.id}': ${String(error)}`)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,32 +168,6 @@ export class WorkspaceRegistry extends Service {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Move one accounted, cwd-validated session to the front of its workspace.
|
||||
* Ungrouped sessions and candidates filtered by the header check are
|
||||
* no-ops. The owning workspace's relative position never changes.
|
||||
* @param sessionId - Session whose activity was observed.
|
||||
* @returns resolution after the possible record write.
|
||||
*/
|
||||
async touchSession(sessionId: SessionId): Promise<void> {
|
||||
const pending = this.pendingTouches.get(sessionId)
|
||||
if (pending !== undefined) {
|
||||
await pending
|
||||
return
|
||||
}
|
||||
for (const entity of this.entities.values()) {
|
||||
if (!entity.hasSession(sessionId)) continue
|
||||
const touch = entity.touchSession(sessionId)
|
||||
this.pendingTouches.set(sessionId, touch)
|
||||
try {
|
||||
await touch
|
||||
} finally {
|
||||
this.pendingTouches.delete(sessionId)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve by canonical directory path without creating or mutating a
|
||||
* workspace. A missing path rejects during `realpath`; an existing unowned
|
||||
|
||||
Reference in New Issue
Block a user