Merge remote-tracking branch 'origin/master' into worktree/attachment-alignment-2

This commit is contained in:
creatixchu
2026-08-12 13:02:39 +08:00
201 changed files with 3681 additions and 1720 deletions

View File

@@ -2522,6 +2522,38 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
emitHost({ type: 'host/workspace-removed', workspaceId })
return ok(request, { deleted: true as const })
},
insertBefore: (request) => {
const { workspaceId, beforeWorkspaceId } = request.payload
const source = workspaces.findIndex(workspace => workspace.workspaceId === workspaceId)
const anchor = beforeWorkspaceId === undefined
? workspaces.length
: workspaces.findIndex(workspace => workspace.workspaceId === beforeWorkspaceId)
const missing = source === -1 ? workspaceId : anchor === -1 ? beforeWorkspaceId : undefined
if (missing !== undefined) {
return err(request, {
code: 'workspace-not-found',
message: `no workspace ${missing}`,
details: { workspaceId: missing },
})
}
if (beforeWorkspaceId !== workspaceId) {
const previousOrder = workspaces.map(candidate => candidate.workspaceId)
const [workspace] = workspaces.splice(source, 1)
/* v8 ignore next -- source was resolved from the same array immediately above. */
if (workspace === undefined) throw new Error(`fixture lost workspace ${workspaceId}`)
const at = beforeWorkspaceId === undefined
? workspaces.length
: workspaces.findIndex(candidate => candidate.workspaceId === beforeWorkspaceId)
workspaces.splice(at, 0, workspace)
if (workspaces.some((candidate, index) => candidate.workspaceId !== previousOrder[index])) {
emitHost({
type: 'host/workspace-order-changed',
workspaceIds: workspaces.map(candidate => candidate.workspaceId),
})
}
}
return ok(request, { workspaceIds: workspaces.map(candidate => candidate.workspaceId) })
},
insertSessionBefore: (request) => {
const { workspaceId, sessionId, beforeSessionId } = request.payload
const workspace = workspaces.find(w => w.workspaceId === workspaceId)
@@ -2968,6 +3000,7 @@ export class FixtureApiClient extends AbstractApiClient {
case 'workspace.create': return this.api.workspace.create(request)
case 'workspace.rename': return this.api.workspace.rename(request)
case 'workspace.delete': return this.api.workspace.delete(request)
case 'workspace.insertBefore': return this.api.workspace.insertBefore(request)
case 'workspace.insertSessionBefore': return this.api.workspace.insertSessionBefore(request)
case 'workspace.archiveSession': return this.api.workspace.archiveSession(request)
case 'skill.list': return this.api.skills.list(request)

View File

@@ -3,7 +3,7 @@
// deferred-controlled timing). Streams are hand pumps: pushMux/pushHost.
import type {
HostFrame, IApiClient, ModelSelection, MuxFrame,
RpcRequest, RpcResponse, SessionId, SessionModels, SessionSearchItem, SkillEntry,
RpcRequest, RpcResponse, SessionId, SessionModels, SessionSearchItem, SkillEntry, WorkspaceId,
} from '../src/client/api.ts'
import { RpcId } from '../src/client/api.ts'
@@ -158,6 +158,9 @@ export class FakeApiClient implements IApiClient {
workspace: { workspaceId: 'fk-ws' as never, path: '/f/ws', title: 'ws', sessionIds: [], createdAt: '0', updatedAt: '0' },
}))),
delete: (payload: unknown) => this.record('workspace.delete', payload, Promise.resolve(ok({ deleted: true as const }))),
insertBefore: (payload: unknown) => this.record('workspace.insertBefore', payload, Promise.resolve(ok({
workspaceIds: [(payload as { workspaceId: WorkspaceId }).workspaceId],
}))),
insertSessionBefore: (payload: unknown) => this.record('workspace.insertSessionBefore', payload, Promise.resolve(ok({
workspace: { workspaceId: 'fk-ws' as never, path: '/f/ws', title: 'ws', sessionIds: [], createdAt: '0', updatedAt: '0' },
}))),

View File

@@ -271,7 +271,12 @@ describe('createFixtureApi', () => {
await consuming
if (!created.result.ok) throw new Error('create failed')
const createdId = created.result.value.sessionId
expect(seen).toEqual([{ type: 'host/session-added', sessionId: createdId, blank: true, cwd: '/tmp/fixture' }])
expect(seen).toHaveLength(1)
const added = seen[0]
if (added?.type !== 'host/session-added') throw new Error('session-added frame missing')
expect(added).toEqual({
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')
expect(list.result.value.items.some(s => s.sessionId === createdId)).toBe(true)
@@ -710,7 +715,11 @@ describe('createFixtureApi', () => {
await consuming
// The session lands with the workspace's path as cwd, and the account
// write pushes the fresh workspace snapshot after session-added.
expect(seen[0]).toEqual({ type: 'host/session-added', sessionId: id, blank: true, cwd: '/tmp/fixture' })
const added = seen[0]
if (added?.type !== 'host/session-added') throw new Error('session-added frame missing')
expect(added).toEqual({
type: 'host/session-added', sessionId: id, blank: true, cwd: '/tmp/fixture',
})
expect(seen[1]).toMatchObject({
type: 'host/workspace-changed',
workspace: { workspaceId: 'fx-ws-fixture', sessionIds: [id, 'fx-alpha', 'fx-beta', 'fx-gamma'] },
@@ -739,7 +748,12 @@ describe('createFixtureApi', () => {
expect(frames[0]).toMatchObject({
type: 'host/workspace-changed', workspace: { sessionIds: [preallocated] },
})
expect(frames[1]).toEqual({ type: 'host/session-added', sessionId: preallocated, blank: true, cwd: made.result.value.workspace.path })
const added = frames[1]
if (added?.type !== 'host/session-added') throw new Error('session-added frame missing')
expect(added).toEqual({
type: 'host/session-added', sessionId: preallocated, blank: true,
cwd: made.result.value.workspace.path,
})
const retried = await api.sessions.create(req({
workspaceId: made.result.value.workspace.workspaceId,