diff --git a/packages/client/connection/src/client/fixture.ts b/packages/client/connection/src/client/fixture.ts index 5b6ccb866d..eaf1beb868 100644 --- a/packages/client/connection/src/client/fixture.ts +++ b/packages/client/connection/src/client/fixture.ts @@ -1387,11 +1387,10 @@ interface FixtureWorld { /** Build the fixture's legacy API and Remote RPC faces over one state graph. */ function createFixtureWorld(options: FixtureOptions): FixtureWorld { // The resident fixture sessions all carry history, so none of them is blank. - const fixtureSessionsNow = Date.now() const sessions: SessionSummary[] = options.empty ? [] : [ - { sessionId: sid('fx-alpha'), createdAt: fixtureSessionsNow - 180_000, updatedAt: fixtureSessionsNow, running: true, blank: false, cwd: '/tmp/fixture' }, - { sessionId: sid('fx-beta'), createdAt: fixtureSessionsNow - 120_000, updatedAt: fixtureSessionsNow - 60_000, running: false, blank: false, parentSessionId: sid('fx-alpha'), cwd: '/tmp/fixture' }, - { sessionId: sid('fx-gamma'), createdAt: fixtureSessionsNow - 60_000, updatedAt: fixtureSessionsNow - 120_000, running: false, blank: false, cwd: '/tmp/fixture' }, + { sessionId: sid('fx-alpha'), updatedAt: Date.now(), running: true, blank: false, cwd: '/tmp/fixture' }, + { sessionId: sid('fx-beta'), updatedAt: Date.now() - 60_000, running: false, blank: false, parentSessionId: sid('fx-alpha'), cwd: '/tmp/fixture' }, + { sessionId: sid('fx-gamma'), updatedAt: Date.now() - 120_000, running: false, blank: false, cwd: '/tmp/fixture' }, ] const logs = new Map([[sid('fx-alpha'), buildAlphaLog()]]) const modelSelections = new Map(sessions.map(session => [ @@ -2047,16 +2046,15 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld { return ok(request, { sessionId: requestedId }) } } - const createdAt = Date.now() const created: SessionSummary = { - sessionId: requestedId ?? sid(`fx-${nextSession++}`), createdAt, updatedAt: createdAt, running: false, blank: true, cwd, + sessionId: requestedId ?? sid(`fx-${nextSession++}`), updatedAt: Date.now(), running: false, blank: true, cwd, } sessions.push(created) modelSelections.set(created.sessionId, { provider: 'deepseek-official', model: 'deepseek-v4-flash' }) attachedSessions += 1 const emitSession = (): void => { // Mirrors the host: the frame fires at creation, so blank is constantly true. - emitHost({ type: 'host/session-added', sessionId: created.sessionId, createdAt, blank: true, cwd }) + emitHost({ type: 'host/session-added', sessionId: created.sessionId, blank: true, cwd }) } if (workspace !== undefined && options.failWorkspaceAttach) { emitSession() @@ -2123,16 +2121,15 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld { } let cut = boundary.seq + 1 while (cut < log.length && log[cut]?.type !== 'turn/start') cut++ - const createdAt = Date.now() const child: SessionSummary = { - sessionId: sid(`fx-${nextSession++}`), createdAt, updatedAt: createdAt, running: false, blank: false, + sessionId: sid(`fx-${nextSession++}`), updatedAt: Date.now(), running: false, blank: false, parentSessionId: sessionId, ...source.cwd === undefined ? {} : { cwd: source.cwd }, } logs.set(child.sessionId, log.slice(0, cut)) sessions.push(child) emitHost({ - type: 'host/session-added', sessionId: child.sessionId, createdAt, blank: false, + type: 'host/session-added', sessionId: child.sessionId, blank: false, parentSessionId: sessionId, ...source.cwd === undefined ? {} : { cwd: source.cwd }, }) diff --git a/packages/client/connection/tests/fixture.spec.ts b/packages/client/connection/tests/fixture.spec.ts index 37ca6ef6e2..109e7acd93 100644 --- a/packages/client/connection/tests/fixture.spec.ts +++ b/packages/client/connection/tests/fixture.spec.ts @@ -267,9 +267,8 @@ describe('createFixtureApi', () => { expect(seen).toHaveLength(1) const added = seen[0] if (added?.type !== 'host/session-added') throw new Error('session-added frame missing') - expect(Number.isFinite(added.createdAt)).toBe(true) expect(added).toEqual({ - type: 'host/session-added', sessionId: createdId, createdAt: added.createdAt, blank: true, cwd: '/tmp/fixture', + type: 'host/session-added', sessionId: createdId, blank: true, cwd: '/tmp/fixture', }) const list = await api.sessions.list(req({})) if (!list.result.ok) throw new Error('list failed') @@ -707,9 +706,8 @@ describe('createFixtureApi', () => { // write pushes the fresh workspace snapshot after session-added. const added = seen[0] if (added?.type !== 'host/session-added') throw new Error('session-added frame missing') - expect(Number.isFinite(added.createdAt)).toBe(true) expect(added).toEqual({ - type: 'host/session-added', sessionId: id, createdAt: added.createdAt, blank: true, cwd: '/tmp/fixture', + type: 'host/session-added', sessionId: id, blank: true, cwd: '/tmp/fixture', }) expect(seen[1]).toMatchObject({ type: 'host/workspace-changed', @@ -741,9 +739,8 @@ describe('createFixtureApi', () => { }) const added = frames[1] if (added?.type !== 'host/session-added') throw new Error('session-added frame missing') - expect(Number.isFinite(added.createdAt)).toBe(true) expect(added).toEqual({ - type: 'host/session-added', sessionId: preallocated, createdAt: added.createdAt, blank: true, + type: 'host/session-added', sessionId: preallocated, blank: true, cwd: made.result.value.workspace.path, }) diff --git a/packages/client/runtime/src/client/contract/sessions-port.ts b/packages/client/runtime/src/client/contract/sessions-port.ts index 5f694921b1..466e26fe12 100644 --- a/packages/client/runtime/src/client/contract/sessions-port.ts +++ b/packages/client/runtime/src/client/contract/sessions-port.ts @@ -16,7 +16,6 @@ export interface SessionsPortSummary { /** Empty-log bit (blank sessions are reused by New Session instead of minting another). */ blank: boolean cwd?: string - createdAt: number updatedAt: number } diff --git a/packages/client/runtime/src/client/sessions/lineage.ts b/packages/client/runtime/src/client/sessions/lineage.ts index 5e54cb59fd..cf8fa0834d 100644 --- a/packages/client/runtime/src/client/sessions/lineage.ts +++ b/packages/client/runtime/src/client/sessions/lineage.ts @@ -17,7 +17,6 @@ export interface TitledSessionSummary extends SessionSummary { export interface SessionListEntry { sessionId: SessionId title?: string - createdAt: number updatedAt: number running: boolean /** Empty-log bit mirrored from the summary; lists hide blank sessions (filtering stays with the consumer). */ @@ -78,7 +77,6 @@ export function flattenLineage( const pendingInteraction = pendingInteractions?.get(s.sessionId) out.push({ ...s, - createdAt: s.createdAt ?? s.updatedAt, ...(pendingInteraction === undefined ? {} : { pendingInteraction }), completed: completed?.has(s.sessionId) ?? false, depth, diff --git a/packages/client/runtime/src/client/sessions/manager.ts b/packages/client/runtime/src/client/sessions/manager.ts index 29e4df2f44..8fbdbdc577 100644 --- a/packages/client/runtime/src/client/sessions/manager.ts +++ b/packages/client/runtime/src/client/sessions/manager.ts @@ -541,9 +541,8 @@ export class SessionManager { : { ...(opts.cwd === undefined ? {} : { cwd: opts.cwd }), ...shared } const { result } = await this.api.sessions.create(payload) if (result.ok) { - const createdAt = Date.now() this.recordMutation({ kind: 'upsert', summary: { - sessionId: result.value.sessionId, createdAt, updatedAt: createdAt, running: false, blank: true, + sessionId: result.value.sessionId, updatedAt: Date.now(), running: false, blank: true, ...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}), ...(result.value.agentPreset !== undefined ? { agentPreset: result.value.agentPreset } : {}), } }) @@ -553,11 +552,9 @@ export class SessionManager { // so expose it immediately as Ungrouped while the caller keeps the // prompt buffer and decides whether to retry attachment. if (publishedSessionId !== undefined) { - const createdAt = Date.now() this.recordMutation({ kind: 'upsert', summary: { sessionId: publishedSessionId, - createdAt, - updatedAt: createdAt, + updatedAt: Date.now(), running: false, blank: true, } }) @@ -591,9 +588,8 @@ export class SessionManager { ? result.value.sessionId : workspaceAttachSessionId(result.error) if (childId !== undefined) { - const createdAt = Date.now() this.recordMutation({ kind: 'upsert', summary: { - sessionId: childId, createdAt, updatedAt: createdAt, running: false, blank: false, + sessionId: childId, updatedAt: Date.now(), running: false, blank: false, parentSessionId: opts.sessionId, ...(source?.cwd !== undefined ? { cwd: source.cwd } : {}), } }) @@ -620,9 +616,8 @@ export class SessionManager { * @param agentPreset - the preset id the host confirmed. */ noteAgentPreset(sessionId: SessionId, agentPreset: string): void { - const createdAt = Date.now() this.recordMutation({ kind: 'upsert', summary: { - sessionId, createdAt, updatedAt: createdAt, running: false, blank: true, agentPreset, + sessionId, updatedAt: Date.now(), running: false, blank: true, agentPreset, } }) } @@ -799,10 +794,8 @@ export class SessionManager { const frame = envelope.payload switch (frame.type) { case 'host/session-added': { - const createdAt = frame.createdAt ?? Date.now() this.mergeSummary({ - sessionId: frame.sessionId, createdAt, updatedAt: createdAt, - running: false, blank: frame.blank, + sessionId: frame.sessionId, updatedAt: Date.now(), running: false, blank: frame.blank, ...(frame.parentSessionId !== undefined ? { parentSessionId: frame.parentSessionId } : {}), ...(frame.origin !== undefined ? { origin: frame.origin } : {}), ...(frame.cwd !== undefined ? { cwd: frame.cwd } : {}), @@ -1055,8 +1048,7 @@ export class SessionManager { const items = fresh.map((entry) => { const prev = this.entryCache.get(entry.sessionId) if ( - prev !== undefined && prev.createdAt === entry.createdAt - && prev.updatedAt === entry.updatedAt && prev.running === entry.running + prev !== undefined && prev.updatedAt === entry.updatedAt && prev.running === entry.running && prev.blank === entry.blank && prev.agentPreset === entry.agentPreset && prev.parentSessionId === entry.parentSessionId && prev.cwd === entry.cwd && prev.origin === entry.origin && prev.title === entry.title && prev.depth === entry.depth diff --git a/packages/client/runtime/src/client/sessions/service.ts b/packages/client/runtime/src/client/sessions/service.ts index c8e5412696..2b7267402e 100644 --- a/packages/client/runtime/src/client/sessions/service.ts +++ b/packages/client/runtime/src/client/sessions/service.ts @@ -66,8 +66,6 @@ export interface SessionSummary { * selected blank entry. */ blank: boolean - /** Durable session creation time. */ - createdAt: number updatedAt: number /** Current host-computed projection values retained by the object layer. */ projectionValues?: Readonly> @@ -669,7 +667,6 @@ export class SessionsService implements ISessions { running: entry.running, ...(entry.completed ? { completed: true } : {}), blank: entry.blank, - createdAt: entry.createdAt, updatedAt: entry.updatedAt, ...(entry.pendingInteraction === undefined ? {} @@ -703,7 +700,6 @@ export class SessionsService implements ISessions { origin: 'subagent', running: child.activity === 'running', blank: false, - createdAt: 0, updatedAt: 0, } } else if (summary.displayTitle !== displayTitle) { diff --git a/packages/client/runtime/tests/subagent-lineage.spec.ts b/packages/client/runtime/tests/subagent-lineage.spec.ts index 9fe99a073f..05881576bf 100644 --- a/packages/client/runtime/tests/subagent-lineage.spec.ts +++ b/packages/client/runtime/tests/subagent-lineage.spec.ts @@ -11,7 +11,7 @@ function summary( running = false, ): SessionSummary { return { - id: sid(id), displayTitle: id, running, blank: false, createdAt: 0, updatedAt: 0, + id: sid(id), displayTitle: id, running, blank: false, updatedAt: 0, ...(parentId === undefined ? {} : { parentId }), ...(origin === undefined ? {} : { origin }), } diff --git a/packages/client/test-runtime/src/sessions.ts b/packages/client/test-runtime/src/sessions.ts index ece67e1acd..fc41c83975 100644 --- a/packages/client/test-runtime/src/sessions.ts +++ b/packages/client/test-runtime/src/sessions.ts @@ -233,7 +233,6 @@ export class TestSessions implements ISessions { displayTitle: fixture.id, running: false, blank: false, - createdAt: this.records.size + 1, updatedAt: this.records.size + 1, ...fixture.summary, } diff --git a/packages/client/ui-conversation/tests/skeleton.spec.tsx b/packages/client/ui-conversation/tests/skeleton.spec.tsx index 9858fdaeb9..be5cd3be28 100644 --- a/packages/client/ui-conversation/tests/skeleton.spec.tsx +++ b/packages/client/ui-conversation/tests/skeleton.spec.tsx @@ -99,10 +99,10 @@ function mount( } = {}, ) { const root = sid('root') - const rootRow = { id: root, displayTitle: 'Root', running: false, blank: false, createdAt: 1, updatedAt: 1 } + const rootRow = { id: root, displayTitle: 'Root', running: false, blank: false, updatedAt: 1 } const childRow = { id: SID, displayTitle: 'Child', parentId: root, cwd: '/projects/one', - running: false, blank: options.summaryBlank ?? false, createdAt: 2, updatedAt: 2, + running: false, blank: options.summaryBlank ?? false, updatedAt: 2, ...(options.summaryOrigin === undefined ? {} : { origin: options.summaryOrigin }), } const listed = options.omitSummaryRow !== true diff --git a/packages/client/ui-subagent/tests/conversation-ui.spec.tsx b/packages/client/ui-subagent/tests/conversation-ui.spec.tsx index 50ec3a6df1..ebc140405e 100644 --- a/packages/client/ui-subagent/tests/conversation-ui.spec.tsx +++ b/packages/client/ui-subagent/tests/conversation-ui.spec.tsx @@ -56,7 +56,6 @@ function props( displayTitle: 'worker', running: true, blank: false, - createdAt: Date.now(), updatedAt: Date.now(), }, }, @@ -84,7 +83,6 @@ function summary(id: SessionId, updatedAt: number): SessionSummary { displayTitle: id, running: false, blank: false, - createdAt: updatedAt, updatedAt, } } diff --git a/packages/client/ui-tool/tests/chat-code-subcalls.spec.tsx b/packages/client/ui-tool/tests/chat-code-subcalls.spec.tsx index 4376694492..5939655118 100644 --- a/packages/client/ui-tool/tests/chat-code-subcalls.spec.tsx +++ b/packages/client/ui-tool/tests/chat-code-subcalls.spec.tsx @@ -110,7 +110,7 @@ async function bench(snapshot: ConversationSnapshot) { const session = createSnapshotStore(snapshot) const list = createSnapshotStore({ ids: [SID], - byId: { [SID]: { id: SID, title: 'S', displayTitle: 'S', running: false, blank: false, createdAt: 1, updatedAt: 1 } }, + byId: { [SID]: { id: SID, title: 'S', displayTitle: 'S', running: false, blank: false, updatedAt: 1 } }, current: SID, phase: 'ready', subagentsByParent: {}, tasksBySession: {}, currentAddress: undefined, }) diff --git a/packages/client/ui-tool/tests/coverage-tails.spec.tsx b/packages/client/ui-tool/tests/coverage-tails.spec.tsx index 06935e0e26..8010fb8763 100644 --- a/packages/client/ui-tool/tests/coverage-tails.spec.tsx +++ b/packages/client/ui-tool/tests/coverage-tails.spec.tsx @@ -26,7 +26,7 @@ function listStore() { return createSnapshotStore({ ids: [SID], byId: { - [SID]: { id: SID, title: 'r', displayTitle: 'r', running: false, blank: false, createdAt: 0, updatedAt: 0 }, + [SID]: { id: SID, title: 'r', displayTitle: 'r', running: false, blank: false, updatedAt: 0 }, }, current: undefined, phase: 'ready', diff --git a/packages/client/ui-tool/tests/diff-card.spec.tsx b/packages/client/ui-tool/tests/diff-card.spec.tsx index 92aab13d8c..1726c81136 100644 --- a/packages/client/ui-tool/tests/diff-card.spec.tsx +++ b/packages/client/ui-tool/tests/diff-card.spec.tsx @@ -156,7 +156,7 @@ describe('chat row diff body', () => { describe('FileMutationRow diff card', () => { const list = () => createSnapshotStore({ ids: [SID], - byId: { [SID]: { id: SID, displayTitle: 'r', running: false, blank: false, createdAt: 0, updatedAt: 0, cwd: '/w/app' } }, + byId: { [SID]: { id: SID, displayTitle: 'r', running: false, blank: false, updatedAt: 0, cwd: '/w/app' } }, current: SID, phase: 'ready', subagentsByParent: {}, tasksBySession: {}, @@ -314,7 +314,7 @@ describe('DetailsPanel diff Output section', () => { ? { ids: [], byId: {}, current: undefined, phase: 'ready', subagentsByParent: {}, tasksBySession: {}, currentAddress: undefined } : { ids: [SID], - byId: { [SID]: { id: SID, displayTitle: 'r', running: false, blank: false, createdAt: 0, updatedAt: 0, cwd } }, + byId: { [SID]: { id: SID, displayTitle: 'r', running: false, blank: false, updatedAt: 0, cwd } }, current: SID, phase: 'ready', subagentsByParent: {}, tasksBySession: {}, diff --git a/packages/client/ui-tool/tests/read-card.spec.tsx b/packages/client/ui-tool/tests/read-card.spec.tsx index bf43507bb3..ae719173d5 100644 --- a/packages/client/ui-tool/tests/read-card.spec.tsx +++ b/packages/client/ui-tool/tests/read-card.spec.tsx @@ -170,7 +170,7 @@ describe('GenericToolCard read body', () => { describe('ReadRow keyed toolview', () => { const list = () => createSnapshotStore({ ids: [SID], - byId: { [SID]: { id: SID, displayTitle: 'r', running: false, blank: false, createdAt: 0, updatedAt: 0, cwd: '/w/app' } }, + byId: { [SID]: { id: SID, displayTitle: 'r', running: false, blank: false, updatedAt: 0, cwd: '/w/app' } }, current: SID, phase: 'ready', subagentsByParent: {}, tasksBySession: {}, @@ -260,7 +260,7 @@ describe('DetailsPanel Output section (read)', () => { ? { ids: [], byId: {}, current: undefined, phase: 'ready', subagentsByParent: {}, tasksBySession: {}, currentAddress: undefined } : { ids: [SID], - byId: { [SID]: { id: SID, displayTitle: 'r', running: false, blank: false, createdAt: 0, updatedAt: 0, cwd } }, + byId: { [SID]: { id: SID, displayTitle: 'r', running: false, blank: false, updatedAt: 0, cwd } }, current: SID, phase: 'ready', subagentsByParent: {}, tasksBySession: {}, diff --git a/packages/client/ui-tool/tests/terminal-card.spec.tsx b/packages/client/ui-tool/tests/terminal-card.spec.tsx index 334da7f607..dd39b8bc88 100644 --- a/packages/client/ui-tool/tests/terminal-card.spec.tsx +++ b/packages/client/ui-tool/tests/terminal-card.spec.tsx @@ -345,7 +345,7 @@ describe('chat row terminal body', () => { describe('BashRow terminal card', () => { const list = () => createSnapshotStore({ ids: [SID], - byId: { [SID]: { id: SID, displayTitle: 'r', running: false, blank: false, createdAt: 0, updatedAt: 0 } }, + byId: { [SID]: { id: SID, displayTitle: 'r', running: false, blank: false, updatedAt: 0 } }, current: undefined, phase: 'ready', subagentsByParent: {}, tasksBySession: {}, @@ -451,7 +451,7 @@ describe('DetailsPanel Output section', () => { ? { ids: [], byId: {}, current: undefined, phase: 'ready', subagentsByParent: {}, tasksBySession: {}, currentAddress: undefined } : { ids: [SID], - byId: { [SID]: { id: SID, displayTitle: 'r', running: false, blank: false, createdAt: 0, updatedAt: 0, cwd } }, + byId: { [SID]: { id: SID, displayTitle: 'r', running: false, blank: false, updatedAt: 0, cwd } }, current: SID, phase: 'ready', subagentsByParent: {}, tasksBySession: {}, diff --git a/packages/client/ui-workspace/README.i18n.yaml b/packages/client/ui-workspace/README.i18n.yaml index 81b52ef012..c5c564c797 100644 --- a/packages/client/ui-workspace/README.i18n.yaml +++ b/packages/client/ui-workspace/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-workspace/README.md -README.md: 24d73beba56f527eb57276accb2693255bad9020 -README.zh.md: 85a5ce9210dd084e6e6e0746eb19aac12d98d54d +README.md: 359979fa26b0e12c7e28b929029fb7d4e9fca324 +README.zh.md: 2139ea3cf7ec3ae84aaa84e181d2f36a60c9ba60 diff --git a/packages/client/ui-workspace/README.md b/packages/client/ui-workspace/README.md index 24d73beba5..359979fa26 100644 --- a/packages/client/ui-workspace/README.md +++ b/packages/client/ui-workspace/README.md @@ -6,7 +6,7 @@ Shared Workspace browser and picker plugin. `WorkspaceBrowser` fills the sidebar The browser renders grouped or flat Session rows from the global runtime hooks and owns Workspace add/rename/reorder plus in-Workspace Session reorder. A Workspace remembers whether it is closed or showing Sessions; an open Workspace shows five Sessions by default, offers a transient **Show more** control for the remainder, and returns to five after the whole Workspace is closed and reopened. View options combine grouping with one browser-persisted Session order: entering **Last updated** performs a complete recency sort and later user prompts or steers promote their Session once, while entering **Manual** preserves every current position and disables later promotion. Dragging edits the current order in either mode; Manual-mode drags also update the Host Workspace account. Workspace drag order is Host-durable in either Session order mode. -Collapsed search is one header action beside the view and add actions. Activating it expands the field across the header; an outside click collapses only an empty query, while the clear control always resets and collapses it. A non-blank search query replaces either browsing mode with one flat result list: case-insensitive title and Workspace substring matches appear immediately, while a 250 ms debounced Host request adds ranked current-conversation content matches and snippets. The English search input and its defensive request path remove NUL, cap the query at the wire schema's 500 UTF-16 code units without splitting a surrogate pair, and preserve the existing debounce and cancellation behavior. Each new query aborts the preceding request; a failed content search leaves metadata matches visible with a warning. The list is capped at 20, asks the user to narrow broader queries, and opens the selected Session without clearing the query or jumping to a specific event. +Collapsed search is one header action beside the view and add actions. Activating it expands the field across the header; an outside click collapses only a query that is empty after trimming, while the clear control always resets and collapses it. A non-blank search query replaces either browsing mode with one flat result list: case-insensitive title and Workspace substring matches appear immediately, while a 250 ms debounced Host request adds ranked current-conversation content matches and snippets. The English search input and its defensive request path remove NUL, cap the query at the wire schema's 500 UTF-16 code units without splitting a surrogate pair, and preserve the existing debounce and cancellation behavior. Each new query aborts the preceding request; a failed content search leaves metadata matches visible with a warning. The list is capped at 20, asks the user to narrow broader queries, and opens the selected Session without clearing the query or jumping to a specific event. The picker lists real Host Workspace entities through the global `useWorkspaces` hook. Selecting a Workspace invokes the slot owner's `onPick` callback to retarget the frontend Session object. Distinct canonical paths remain separate id-keyed Workspaces when their basenames and display titles match; the sidebar hover detail exposes the full path. Each registration declares a **directory-flow child hole** (`single` kind: `conversation.hero.workspace.directoryFlow` / `sidebar.workspaces.directoryFlow`) that the composed picker package's client half fills with its picking interaction — the [`-native`](../../host/directory-picker-native/README.md) backend's renderless OS-chooser driver today, an in-app browsing dialog under a `-browse` composition. The flat **Add workspace...** action renders only while the surface's hole is occupied (occupancy read per menu render; an empty hole means the composition has no picking affordance — the seam's documented no-flow default, under which the sidebar header drops its add button rather than offering a dead one). This package owns the trigger and the adoption: the occupant reports one picked path per open through the hole's owner conversation (`open`/`busy`/`onPicked`/`onCancel`/`onError`), and the owner adopts it through the object layer, selecting the committed Workspace only after its list projection has refreshed; cancellation is silent, and errors land in the retryable folder dialog whose **Choose again** reopens the flow. Adding has exactly one route: the occupant's own create-folder affordance already covers a brand-new directory, so no separate create-by-name dialog exists. A menu only appears where there is something to choose between — with no Workspace listed, the anchor gesture raises the flow directly instead of a one-row popover, and it waits for the list baseline before treating an empty list as final. The runtime Session and Workspace services own materialization. The Workspace row's Delete action opens a confirmation that states the retention boundary, blocks duplicate submission, and keeps failures open; success removes the group while its Sessions remain under Ungrouped. The Session row's Rename action opens the same browser-owned dialog pattern prefilled with the row's display title: no client-side conflict rule exists (the host normalizes and may reject with `title-invalid`, rendered in the dialog alert), and confirming an unchanged title is deliberately allowed — it pins the current automatic title against regeneration. The Session row's Archive action commits without a confirmation dialog (non-destructive: the log and the workspace accounting slot remain) through `ctx.workspaces.archiveSession`; the row disappears from every grouping surface — workspace groups, Ungrouped, content search, and the flat list — when the archive-set echo lands, and failures are console diagnostics that leave the tree unchanged. A blank New Session row is a pure placeholder: it renders no row menu and no time label (nothing has happened in it yet), so rename, fork, and archive first apply once the first prompt lands. diff --git a/packages/client/ui-workspace/README.zh.md b/packages/client/ui-workspace/README.zh.md index 85a5ce9210..2139ea3cf7 100644 --- a/packages/client/ui-workspace/README.zh.md +++ b/packages/client/ui-workspace/README.zh.md @@ -6,7 +6,7 @@ 该浏览器通过全局运行时钩子将 Session 行渲染为分组或扁平形式,并负责 Workspace 添加/重命名/重排序以及 Workspace 内的 Session 重排序。每个 Workspace 会记住自身是关闭还是显示 Session;打开后默认显示五条 Session,其余条目通过临时的**展开其余**控件显示,而关闭并重新打开整个 Workspace 后会恢复为五条。视图选项把分组方式和一份浏览器持久化的 Session 顺序放在一起:进入**最近更新**时执行一次完整的时间排序,后续 user prompt 或 steer 会将对应 Session 置顶一次;进入**手动排序**则保留所有当前位置并停用后续置顶。两种模式下的拖拽都会编辑当前顺序,手动模式下的拖拽还会更新 Host Workspace 记账。无论采用哪种 Session 顺序,Workspace 拖拽顺序都由 Host 持久化。 -折叠搜索是视图和添加操作旁的一枚区头按钮。激活后,输入框会扩展并占据区头;点击外部只会收起空查询,而清除控件总会重置并收起搜索。非空白查询会以单一扁平结果列表替代任一浏览模式:不区分大小写的标题和 Workspace 子串匹配项会立即显示,经 250 ms 防抖的 Host 请求则会加入经过排序的当前对话内容匹配项及其摘要片段。英文搜索输入框及其防御性请求路径会移除 NUL,将查询限制在传输 schema 规定的 500 个 UTF-16 代码单元内且不会拆分代理项对,并保留现有的防抖与取消行为。每次新查询都会中止前一个请求;内容搜索失败时,元数据匹配项仍会显示,同时给出警告。列表最多显示 20 条结果,并会在查询过宽时提示用户缩小范围;打开所选 Session 时既不会清除查询,也不会跳转至特定事件。 +折叠搜索是视图和添加操作旁的一枚区头按钮。激活后,输入框会扩展并占据区头;点击外部只会收起经清除首尾空白后为空的查询,而清除控件总会重置并收起搜索。非空白查询会以单一扁平结果列表替代任一浏览模式:不区分大小写的标题和 Workspace 子串匹配项会立即显示,经 250 ms 防抖的 Host 请求则会加入经过排序的当前对话内容匹配项及其摘要片段。英文搜索输入框及其防御性请求路径会移除 NUL,将查询限制在传输 schema 规定的 500 个 UTF-16 代码单元内且不会拆分代理项对,并保留现有的防抖与取消行为。每次新查询都会中止前一个请求;内容搜索失败时,元数据匹配项仍会显示,同时给出警告。列表最多显示 20 条结果,并会在查询过宽时提示用户缩小范围;打开所选 Session 时既不会清除查询,也不会跳转至特定事件。 该选择器通过全局 `useWorkspaces` hook 列出真实的 Host Workspace 实体。选择 Workspace 会调用 slot owner 的 `onPick` 回调,重新定位前端 Session 对象。不同的规范化路径即使 basename 和显示标题相同,仍会作为由 id 区分的独立 Workspace;侧边栏的悬停详情会显示完整路径。每个注册各自声明一个**目录流子 slot**(`single` kind:`conversation.hero.workspace.directoryFlow`/`sidebar.workspaces.directoryFlow`),由组合的选择器包 client half 填入其选取交互——今天是 [`-native`](../../host/directory-picker-native/README.md) 后端的无渲染 OS 选择器驱动,`-browse` 组合下则是应用内浏览对话框。平铺显示的 **添加工作区…** 操作仅在当前界面的 slot 被占用时渲染(每次菜单渲染读取占用状态;slot 为空意味着该组合没有目录选择能力——seam 文档化的无流程默认行为,此时侧边栏区头直接不渲染添加按钮,而非留下一个点了没反应的按钮)。本包持有触发与接纳:占用方通过 slot 的属主交互约定(`open`/`busy`/`onPicked`/`onCancel`/`onError`)每次打开上报一个所选路径,owner 通过对象层接纳它,并等待 Workspace 列表投影刷新后才选中已提交的 Workspace;取消操作不会显示提示,错误落入可重试的文件夹对话框,其 **重新选择** 会重新打开流程。添加只有一条路径:占用者自带的新建文件夹能力已经覆盖了全新目录,因此不再单设按名称创建的对话框。菜单只在确有多个目标可选时出现——没有 Workspace 可列时,锚点手势直接拉起流程,而不是弹出只有一行的浮层;在列表基线落地前,空列表不算最终结果。运行时 Session 与 Workspace 服务负责物化。Workspace 行内的 Delete 操作会打开确认框,说明保留边界、阻止重复提交,并在失败时保持打开;成功后,该分组会被移除,其 Session 则留在 Ungrouped 下。Session 行内的 Rename 操作打开同款浏览器持有的对话框,并以该行的显示标题预填:客户端不设名称冲突规则(host 负责规范化,可能以 `title-invalid` 拒绝,错误渲染在对话框告警区);确认未修改的标题是有意允许的——这正是把当前自动标题钉住、不再被重新生成覆盖的手势。Session 行内的 Archive 操作不经确认对话框直接提交(非破坏性:日志和 workspace 记账席位保持不变),通过 `ctx.workspaces.archiveSession` 归档;归档集合回声落地后,该行从所有分组视图——workspace 分组、Ungrouped、内容搜索和平铺列表——中消失,失败只作为控制台诊断输出,树保持不变。空白的「新会话」行只是占位符:不渲染行菜单和时间标签(其中还没有发生任何事),重命名、fork 和归档都从首条提示词落地后才可用。 diff --git a/packages/client/ui-workspace/src/client/WorkspaceBrowser.module.css b/packages/client/ui-workspace/src/client/WorkspaceBrowser.module.css index d9eeb50c9a..43170e90a0 100644 --- a/packages/client/ui-workspace/src/client/WorkspaceBrowser.module.css +++ b/packages/client/ui-workspace/src/client/WorkspaceBrowser.module.css @@ -38,19 +38,6 @@ background: var(--dsw-alias-interactive-bg-hover); } -.viewOptionLabel { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - width: 100%; -} - -.viewOptionCheck { - flex: none; - color: var(--dsw-alias-label-primary); -} - /* Section header: title, an inline search control, and the two trailing actions. Expanding search collapses the action cluster and takes its room. */ .sectionHeader { diff --git a/packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx b/packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx index cbc2e05159..f7b32c95b6 100644 --- a/packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx +++ b/packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx @@ -12,7 +12,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import clsx from 'clsx' import { - Button, IconCheckOutline16, IconCloseFill14, IconPersonalizationOutline16, + Button, IconCloseFill14, IconPersonalizationOutline16, IconProjectAddOutline16, IconSearchOutline16, Menu, Modal, Tooltip, } from '@deepseek-ai/dsh-client-ui-primitives' import type { @@ -89,24 +89,19 @@ function ViewOptionsMenu({ groupBy, orderBy, onGroupPick, onOrderPick, t }: { t: WorkspaceBrowserProps['t'] }) { const [open, setOpen] = useState(false) - const optionLabel = (label: string, selected: boolean) => ( - - {label} - {selected && } - - ) return ( { setOpen(false) }} items={[ { type: 'label' as const, id: 'group-by', text: t('groupBy.label') }, - { id: 'workspace', label: optionLabel(t('groupBy.workspace'), groupBy === 'workspace') }, - { id: 'flat', label: optionLabel(t('groupBy.flat'), groupBy === 'flat') }, + { id: 'workspace', label: t('groupBy.workspace') }, + { id: 'flat', label: t('groupBy.flat') }, { type: 'label' as const, id: 'order-by', text: t('orderBy.label') }, - { id: 'manual', label: optionLabel(t('orderBy.manual'), orderBy === 'manual'), disabled: groupBy !== 'workspace' }, - { id: 'updated', label: optionLabel(t('orderBy.updated'), orderBy === 'updated') }, + { id: 'manual', label: t('orderBy.manual'), disabled: groupBy !== 'workspace' }, + { id: 'updated', label: t('orderBy.updated') }, ]} + selectedIds={[groupBy, orderBy]} onSelect={(id) => { if (id === 'workspace' || id === 'flat') onGroupPick(id) else if (id === 'manual' || id === 'updated') onOrderPick(id) @@ -280,7 +275,7 @@ function SessionTree({ }) }, [recentSessionOrder, workspaces]) const groups = useMemo( - () => deriveGroups(list, orderedWorkspaces, archivedSessionIds, { expandedProjects }, 'manual'), + () => deriveGroups(list, orderedWorkspaces, archivedSessionIds, { expandedProjects }), [list, orderedWorkspaces, archivedSessionIds, expandedProjects], ) const now = Date.now() @@ -684,12 +679,12 @@ export function WorkspaceBrowser({ const onClick = (event: MouseEvent): void => { if (!(event.target instanceof Node) || searchRoot.current?.contains(event.target) === true) return searchInput.current?.blur() - if (query !== '') return + if (normalizedQuery !== '') return setSearchExpanded(false) } document.addEventListener('click', onClick) return () => { document.removeEventListener('click', onClick) } - }, [query, wide, searchExpanded]) + }, [normalizedQuery, wide, searchExpanded]) useEffect(() => { if (normalizedQuery === '') { diff --git a/packages/client/ui-workspace/src/client/tree.ts b/packages/client/ui-workspace/src/client/tree.ts index 292b574b2f..69de67aeee 100644 --- a/packages/client/ui-workspace/src/client/tree.ts +++ b/packages/client/ui-workspace/src/client/tree.ts @@ -29,7 +29,6 @@ export interface SessionNode { runningSubagentCount: number /** Finished running while not selected and not yet opened (the green "done" reminder dot). */ completed: boolean - createdAt: number updatedAt: number } @@ -108,10 +107,6 @@ function byRecency(a: SessionSummary, b: SessionSummary): number { return a.id < b.id ? -1 : 1 } -function sortSessions(sessions: SessionSummary[]): void { - sessions.sort(byRecency) -} - /** * Ordinary sessions are visible; among blank sessions, only the current one * is visible. Subagent children use their parent header catalog; archived @@ -141,10 +136,12 @@ function buildGroup( createdAt: number | undefined, label: string, members: readonly SessionSummary[], - orderBy: SessionOrderBy, + order: 'account' | 'recency', ): Group { const sessions = [...members] - if (orderBy !== 'manual') sortSessions(sessions) + // Workspace order is the caller-selected sessionIds; only Ungrouped lacks + // an account order and therefore falls back to recency. + if (order === 'recency') sessions.sort(byRecency) return { key, workspaceId, cwd, createdAt, label, sessions } } @@ -157,7 +154,6 @@ function groupByWorkspace( list: SessionListState, workspaces: readonly WorkspaceView[], archived: ReadonlySet, - orderBy: SessionOrderBy, ): Group[] { const groups: Group[] = [] const accounted = new Set() @@ -172,7 +168,7 @@ function groupByWorkspace( } groups.push(buildGroup( workspace.workspaceId, workspace.workspaceId, workspace.path, - Date.parse(workspace.createdAt), workspace.title, members, orderBy, + Date.parse(workspace.createdAt), workspace.title, members, 'account', )) } const stray = list.ids @@ -180,10 +176,7 @@ function groupByWorkspace( .filter((s): s is SessionSummary => s !== undefined && !accounted.has(s.id) && sessionVisible(s, list.current, archived)) if (stray.length > 0) { - groups.push(buildGroup( - UNGROUPED_KEY, undefined, undefined, undefined, UNGROUPED_LABEL, stray, - orderBy === 'manual' ? 'updated' : orderBy, - )) + groups.push(buildGroup(UNGROUPED_KEY, undefined, undefined, undefined, UNGROUPED_LABEL, stray, 'recency')) } return groups } @@ -199,7 +192,6 @@ function sessionNode( running: s.running, runningSubagentCount: descendants.get(s.id)?.runningCount ?? 0, completed: s.completed === true, - createdAt: s.createdAt, updatedAt: s.updatedAt, ...(s.pendingInteraction === undefined ? {} : { pendingInteraction: s.pendingInteraction }), } @@ -217,7 +209,6 @@ function sessionNode( * @param workspaces - real workspaces in stable Host order. * @param archivedSessionIds - registry-global archive set. * @param view - local expansion arrays. - * @param orderBy - local session ordering mode. * @returns group sections in render order. */ export function deriveGroups( @@ -225,7 +216,6 @@ export function deriveGroups( workspaces: readonly WorkspaceView[], archivedSessionIds: readonly SessionId[], view: TreeView, - orderBy: SessionOrderBy = 'manual', ): GroupNode[] { const archived = new Set(archivedSessionIds) const expandedProjects = new Set(view.expandedProjects) @@ -235,7 +225,7 @@ export function deriveGroups( : (workspaces.find(w => w.sessionIds.includes(list.current as SessionId))?.workspaceId as string | undefined) ?? UNGROUPED_KEY const groups: GroupNode[] = [] - for (const g of groupByWorkspace(list, workspaces, archived, orderBy)) { + for (const g of groupByWorkspace(list, workspaces, archived)) { const expanded = expandedProjects.has(g.key) groups.push({ key: g.key, @@ -273,7 +263,7 @@ export function deriveFlat( if (s === undefined || !sessionVisible(s, list.current, archived)) continue rows.push(s) } - sortSessions(rows) + rows.sort(byRecency) return rows.map(session => sessionNode(session, descendants)) } diff --git a/packages/client/ui-workspace/tests/rows.spec.tsx b/packages/client/ui-workspace/tests/rows.spec.tsx index edb60c23bb..c436cab7a5 100644 --- a/packages/client/ui-workspace/tests/rows.spec.tsx +++ b/packages/client/ui-workspace/tests/rows.spec.tsx @@ -116,7 +116,7 @@ describe('workspace browser rows', () => { it('renders and opens a selected running Session row', () => { const node: SessionNode = { id: sid('session'), title: 'Session', blank: false, running: true, - runningSubagentCount: 0, completed: false, createdAt: 0, updatedAt: 0, + runningSubagentCount: 0, completed: false, updatedAt: 0, } const onOpen = vi.fn() render( @@ -137,7 +137,7 @@ describe('workspace browser rows', () => { { try { const node: SessionNode = { id: sid('owner'), title: 'Delegating', blank: false, running: false, - runningSubagentCount: 2, completed: false, createdAt: 0, updatedAt: 0, + runningSubagentCount: 2, completed: false, updatedAt: 0, } render() @@ -191,7 +191,7 @@ describe('workspace browser rows', () => { try { const node: SessionNode = { id: sid('owner'), title: 'Delegating', blank: false, running: true, - runningSubagentCount: 1, completed: false, createdAt: 0, updatedAt: 0, + runningSubagentCount: 1, completed: false, updatedAt: 0, } render() @@ -212,7 +212,7 @@ describe('workspace browser rows', () => { it('keeps child activity as a secondary status while user attention is primary', () => { const node: SessionNode = { id: sid('owner'), title: 'Needs input', blank: false, pendingInteraction: 'question', - running: false, runningSubagentCount: 1, completed: false, createdAt: 0, updatedAt: 0, + running: false, runningSubagentCount: 1, completed: false, updatedAt: 0, } render() @@ -303,7 +303,7 @@ describe('workspace browser rows', () => { try { const node: SessionNode = { id: sid('s-blank'), title: 'ignored', blank: true, running: false, - runningSubagentCount: 0, completed: false, createdAt: 0, updatedAt: 0, + runningSubagentCount: 0, completed: false, updatedAt: 0, } render() @@ -330,7 +330,7 @@ describe('workspace browser rows', () => { const onArchive = vi.fn() const node: SessionNode = { id: sid('s1'), title: 'One', blank: false, running: false, - runningSubagentCount: 0, completed: false, createdAt: 0, updatedAt: 0, + runningSubagentCount: 0, completed: false, updatedAt: 0, } render() @@ -364,7 +364,7 @@ describe('workspace browser rows', () => { try { const node: SessionNode = { id: sid('s1'), title: 'Hovered', blank: false, running: true, - runningSubagentCount: 0, completed: false, createdAt: 0, updatedAt: 0, + runningSubagentCount: 0, completed: false, updatedAt: 0, } render() @@ -395,7 +395,7 @@ describe('workspace browser rows', () => { try { const node: SessionNode = { id: sid(pendingInteraction), title: 'Needs input', blank: false, - pendingInteraction, running: true, runningSubagentCount: 0, completed: false, createdAt: 0, updatedAt: 0, + pendingInteraction, running: true, runningSubagentCount: 0, completed: false, updatedAt: 0, } const view = render() @@ -422,7 +422,7 @@ describe('workspace browser rows', () => { try { const node: SessionNode = { id: sid('s1'), title: 'Quiet', blank: false, running: false, - runningSubagentCount: 0, completed: false, createdAt: 0, updatedAt: 0, + runningSubagentCount: 0, completed: false, updatedAt: 0, } render() @@ -440,7 +440,7 @@ describe('workspace browser rows', () => { try { const node: SessionNode = { id: sid('s1'), title: 'Done', blank: false, running: false, - runningSubagentCount: 0, completed: true, createdAt: 0, updatedAt: 0, + runningSubagentCount: 0, completed: true, updatedAt: 0, } render() @@ -456,7 +456,7 @@ describe('workspace browser rows', () => { it('draggable row wires start/end and gates hover/drop on an active same-group drag', () => { const node: SessionNode = { id: sid('s1'), title: 'Drag me', blank: false, running: false, - runningSubagentCount: 0, completed: false, createdAt: 0, updatedAt: 0, + runningSubagentCount: 0, completed: false, updatedAt: 0, } const inactive = dragProps() const { rerender } = render( diff --git a/packages/client/ui-workspace/tests/tree.spec.ts b/packages/client/ui-workspace/tests/tree.spec.ts index 4ee5c19561..5ba8412193 100644 --- a/packages/client/ui-workspace/tests/tree.spec.ts +++ b/packages/client/ui-workspace/tests/tree.spec.ts @@ -12,7 +12,7 @@ const sid = (id: string) => id as SessionId const wid = (id: string) => id as WorkspaceId const summary = (id: string, updatedAt: number, cwd?: string): SessionSummary => ({ id: sid(id), displayTitle: id, running: false, blank: false, - createdAt: updatedAt, updatedAt, ...(cwd === undefined ? {} : { cwd }), + updatedAt, ...(cwd === undefined ? {} : { cwd }), }) const list = (...items: SessionSummary[]): SessionListState => ({ ids: items.map(item => item.id), diff --git a/packages/client/ui-workspace/tests/workspace-browser.spec.tsx b/packages/client/ui-workspace/tests/workspace-browser.spec.tsx index be567b0b74..0c4a1a9104 100644 --- a/packages/client/ui-workspace/tests/workspace-browser.spec.tsx +++ b/packages/client/ui-workspace/tests/workspace-browser.spec.tsx @@ -22,7 +22,7 @@ const t: WorkspaceBrowserProps['t'] = makeTranslate(zh, commonZh) const sid = (id: string) => id as SessionId const wid = (id: string) => id as WorkspaceId const summary = (id: string, updatedAt: number, overrides: Partial = {}): SessionSummary => ({ - id: sid(id), displayTitle: id, running: false, blank: false, createdAt: updatedAt, updatedAt, ...overrides, + id: sid(id), displayTitle: id, running: false, blank: false, updatedAt, ...overrides, }) const sessionState = (items: readonly SessionSummary[], overrides: Partial = {}): SessionListState => ({ ids: items.map(item => item.id), @@ -107,8 +107,11 @@ describe('WorkspaceBrowser', () => { fireEvent.click(screen.getByRole('button', { name: '视图选项' })) expect(screen.getByText('分组方式')).toBeTruthy() // the menu heading label + expect(screen.getAllByRole('menuitem').map(item => item.textContent)).toEqual([ + '按工作区', '单列表', '手动排序', '最近更新', + ]) + expect(screen.getByRole('menuitem', { name: '按工作区' }).querySelector('svg')).toBeTruthy() expect(screen.getByRole('menuitem', { name: '手动排序' }).querySelector('svg')).toBeTruthy() - expect(screen.queryByText('创建时间')).toBeNull() fireEvent.click(screen.getByRole('menuitem', { name: '单列表' })) // Store-driven flip: title changes, rows flatten newest-first, headers gone. expect(b.store.getSnapshot().groupBy).toBe('flat') @@ -405,6 +408,11 @@ describe('WorkspaceBrowser', () => { fireEvent.click(search) const input = screen.getByPlaceholderText('搜索会话…') + fireEvent.change(input, { target: { value: ' ' } }) + fireEvent.click(document.body) + expect(search.getAttribute('aria-expanded')).toBe('false') + + fireEvent.click(search) fireEvent.change(input, { target: { value: 'kept' } }) fireEvent.click(document.body) expect(search.getAttribute('aria-expanded')).toBe('true') diff --git a/packages/host/apiproxy/src/api-proxy.ts b/packages/host/apiproxy/src/api-proxy.ts index f21d3e9a86..0a9a4e1fbd 100644 --- a/packages/host/apiproxy/src/api-proxy.ts +++ b/packages/host/apiproxy/src/api-proxy.ts @@ -466,7 +466,6 @@ function sessionListFields(header: SessionHeader, events: readonly SessionEvent[ function summarize(session: Session, running: boolean): SessionSummary { return { sessionId: session.id, - createdAt: session.header.createdAt, // Excludes end-seed: a resumed-but-untouched session // must not sort as freshly worked in. updatedAt: lastActivityTime(session.events) ?? session.header.createdAt, @@ -500,7 +499,6 @@ async function summarizeCold( } return { sessionId: meta.id, - createdAt: meta.createdAt, updatedAt, running: false, // Lazy persistence keeps never-appended sessions out of list(); reading @@ -3394,7 +3392,6 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro queue.push(frame({ type: 'host/session-added', sessionId: session.id, - createdAt: session.header.createdAt, // Derived at frame time like summarize(); a just-created session // has run no turn yet, so this is constantly true in practice. blank: sessionBlank(session), diff --git a/packages/host/apiproxy/src/api/events.schema.ts b/packages/host/apiproxy/src/api/events.schema.ts index 4bdd8d3a24..66d8208ee6 100644 --- a/packages/host/apiproxy/src/api/events.schema.ts +++ b/packages/host/apiproxy/src/api/events.schema.ts @@ -71,7 +71,6 @@ export const hostFrameSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('host/session-added'), sessionId: sessionIdSchema, - createdAt: z.number().optional(), blank: z.boolean(), parentSessionId: sessionIdSchema.optional(), origin: z.literal('subagent').optional(), diff --git a/packages/host/apiproxy/src/api/events.ts b/packages/host/apiproxy/src/api/events.ts index 5a116a1f9d..c6373523f2 100644 --- a/packages/host/apiproxy/src/api/events.ts +++ b/packages/host/apiproxy/src/api/events.ts @@ -128,7 +128,6 @@ export type HostFrame = | { type: 'host/session-added' sessionId: SessionId - createdAt?: number blank: boolean parentSessionId?: SessionId origin?: 'subagent' diff --git a/packages/host/apiproxy/src/api/sessions.schema.ts b/packages/host/apiproxy/src/api/sessions.schema.ts index fc69637363..56572fb821 100644 --- a/packages/host/apiproxy/src/api/sessions.schema.ts +++ b/packages/host/apiproxy/src/api/sessions.schema.ts @@ -51,7 +51,6 @@ export const sessionEventSchema = z.object({ /** SessionSummary row of session.list (`projections` reuses the history block's shape and schema). */ export const sessionSummarySchema = z.object({ sessionId: sessionIdSchema, - createdAt: z.number().optional(), updatedAt: z.number(), running: z.boolean(), blank: z.boolean(), diff --git a/packages/host/apiproxy/src/api/sessions.ts b/packages/host/apiproxy/src/api/sessions.ts index 24272dcf07..d1f0317e8f 100644 --- a/packages/host/apiproxy/src/api/sessions.ts +++ b/packages/host/apiproxy/src/api/sessions.ts @@ -147,8 +147,6 @@ export type QueueAction = /** Session list entry (v1 builds no index: list does readdir+stat). */ export interface SessionSummary { sessionId: SessionId - /** Session creation time from the durable session header when supplied by the Host. */ - createdAt?: number /** * Last activity. Attached: the last non-`session/end-seed` event, since a * pickup is not activity. Cold: the log's mtime, or `createdAt` for a backend