Merge remote-tracking branch 'origin/master' into mergebot/pr711

# Conflicts:
#	apps/cli/README.i18n.yaml
#	apps/cli/README.md
#	apps/cli/README.zh.md
#	apps/cli/cordis.yml
#	apps/cli/package.json
#	docs/config-catalog.md
#	packages/client/runtime/README.i18n.yaml
#	packages/client/runtime/src/client/contract/sessions.ts
#	packages/client/test-runtime/src/sessions.ts
#	packages/client/ui-workspace/README.i18n.yaml
#	packages/client/ui-workspace/README.md
#	packages/client/ui-workspace/README.zh.md
#	packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx
#	packages/client/ui-workspace/src/client/tree.ts
#	packages/client/ui-workspace/tests/apply.spec.ts
#	packages/client/ui-workspace/tests/tree.spec.ts
#	packages/host/apiproxy/README.i18n.yaml
#	packages/host/apiproxy/src/api-proxy.ts
#	packages/host/apiproxy/src/api/index.ts
#	packages/host/apiproxy/tests/client-handler.spec.ts
#	packages/host/apiproxy/tests/rpc-schemas.spec.ts
#	pnpm-lock.yaml
This commit is contained in:
imccyu
2026-07-31 01:28:15 +08:00
980 changed files with 30720 additions and 7127 deletions

View File

@@ -12,7 +12,7 @@ export type {
WorkspaceApi, WorkspaceId, WorkspaceView,
CommandsApi, CommandDescriptor, SkillsApi, SkillEntry,
ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,
ModelReasoningEffort, ModelTarget, SessionModels,
InboxItemId, ModelReasoningEffort, ModelTarget, QueueAction, QueuedInboxItem, SessionModels,
GoalsApi, GoalRef,
} from '@deepseek-ai/dsh-host-apiproxy/api'
export type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-tools/presentation'

View File

@@ -1220,6 +1220,56 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
const appended = logOf(sessionId).at(-1) as SessionEvent
return ok(request, { title: normalized, seq: appended.seq })
},
fork: (request) => {
const { sessionId, atSeq } = request.payload
const source = summaryOf(sessionId)
if (source === undefined) {
return err(request, {
code: 'session-not-found',
message: `no session ${sessionId}`,
details: { sessionId },
})
}
const log = logs.get(sessionId) ?? []
const lastSeq = log.at(-1)?.seq ?? -1
const anchoredBoundary = atSeq === undefined
? undefined
: log.find(e => e.type === 'turn/end' && e.seq >= atSeq)
const boundary = anchoredBoundary
?? (atSeq === undefined || atSeq > lastSeq
? log.findLast(e => e.type === 'turn/end')
: undefined)
if (boundary === undefined) {
return err(request, {
code: 'fork-unavailable',
message: atSeq !== undefined && atSeq <= lastSeq
? `session ${sessionId} has not completed the turn containing event ${String(atSeq)}`
: `session ${sessionId} has no completed turn`,
details: { sessionId },
})
}
let cut = boundary.seq + 1
while (cut < log.length && log[cut]?.type !== 'turn/start') cut++
const child: SessionSummary = {
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, blank: false,
parentSessionId: sessionId,
...source.cwd === undefined ? {} : { cwd: source.cwd },
})
const workspace = workspaces.find(w => w.sessionIds.includes(sessionId))
if (workspace !== undefined) {
workspace.sessionIds = [child.sessionId, ...workspace.sessionIds]
workspace.updatedAt = new Date().toISOString()
emitHost({ type: 'host/workspace-changed', workspace: { ...workspace } })
}
return ok(request, { sessionId: child.sessionId })
},
history: async (request) => {
const log = logs.get(request.payload.sessionId) ?? []
// Snapshot at request time, deliver after the transit delay (mirrors a real host under latency).
@@ -1328,6 +1378,11 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
)
return ok(request, { accepted: true as const })
},
updateQueue: request => err(request, {
code: 'queue-item-not-found',
message: 'fixture has no pending queue item',
details: { itemId: request.payload.itemId },
}),
cancel: (request) => {
const replay = replays.get(request.payload.sessionId)
if (replay !== undefined) {
@@ -1774,7 +1829,9 @@ export class FixtureApiClient extends AbstractApiClient {
case 'session.models': return this.api.sessions.models(request)
case 'session.selectModel': return this.api.sessions.selectModel(request)
case 'session.rename': return this.api.sessions.rename(request)
case 'session.fork': return this.api.sessions.fork(request)
case 'session.prompt': return this.api.sessions.prompt(request)
case 'session.updateQueue': return this.api.sessions.updateQueue(request)
case 'session.cancel': return this.api.sessions.cancel(request)
case 'host.describe': return this.api.host.describe(request)
case 'host.pickDirectory': return this.api.host.pickDirectory(request, new AbortController().signal)

View File

@@ -17,7 +17,7 @@ export type {
ToolCallView, ToolResultView, WorkspaceApi, WorkspaceId, WorkspaceView,
CommandsApi, CommandDescriptor, SkillsApi, SkillEntry,
ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,
ModelReasoningEffort, ModelTarget, SessionModels,
InboxItemId, ModelReasoningEffort, ModelTarget, QueueAction, QueuedInboxItem, SessionModels,
RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode,
ClientRequest, ServerResponse, ServerRequest, ClientResponse, RpcMessage, RpcReceipt,
IApiClient, SessionId, SessionEvent, ContentBlock, StreamChunk,

View File

@@ -48,6 +48,7 @@ export class FakeApiClient implements IApiClient {
() => Promise.resolve(ok({ items: [], hasMore: false }))
onCreate: (payload: unknown) => Promise<RpcResponse<{ sessionId: SessionId }>> = () => Promise.resolve(ok({ sessionId: 'fk-new' as SessionId }))
onRename: (payload: unknown) => Promise<RpcResponse<{ title: string; seq: number }>> = () => Promise.resolve(ok({ title: 'fk-renamed', seq: 0 }))
onFork: (payload: unknown) => Promise<RpcResponse<{ sessionId: SessionId }>> = () => Promise.resolve(ok({ sessionId: 'fk-fork' as SessionId }))
onHistory: (payload: { sessionId: SessionId; beforeSeq?: number; maxMessages?: number })
=> Promise<RpcResponse<{ events: never[]; hasMore: boolean; modelTarget: ModelTarget }>> =
() => Promise.resolve(ok({
@@ -65,6 +66,7 @@ export class FakeApiClient implements IApiClient {
=> Promise<RpcResponse<{ selected: ModelTarget }>> =
payload => Promise.resolve(ok({ selected: { provider: payload.provider, model: payload.model } }))
onPrompt: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
onUpdateQueue: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
onCancel: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
onDescribe: (payload: unknown) => Promise<RpcResponse<{ version: string; cwd: string; attachedSessions: number }>> =
() => Promise.resolve(ok({ version: '0-fake', cwd: '/f', attachedSessions: 0 }))
@@ -105,7 +107,9 @@ export class FakeApiClient implements IApiClient {
selectModel: (payload: ModelTarget & { sessionId: SessionId }) =>
this.record('session.selectModel', payload, this.onSelectModel(payload)),
rename: (payload: unknown) => this.record('session.rename', payload, this.onRename(payload)),
fork: (payload: unknown) => this.record('session.fork', payload, this.onFork(payload)),
prompt: (payload: unknown) => this.record('session.prompt', payload, this.onPrompt(payload)),
updateQueue: (payload: unknown) => this.record('session.updateQueue', payload, this.onUpdateQueue(payload)),
cancel: (payload: unknown) => this.record('session.cancel', payload, this.onCancel(payload)),
}