Merge remote-tracking branch 'origin/master' into worktree/web-session-model-selector
# Conflicts: # packages/client/runtime/README.i18n.yaml # packages/host/apiproxy/README.i18n.yaml # packages/host/apiproxy/README.md # packages/host/apiproxy/README.zh.md
This commit is contained in:
@@ -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/host/apiproxy/README.md
|
||||
README.md: 296ce8dd999aa251f87a9dbadb2ef821fe58e173
|
||||
README.zh.md: b6191ac4b096af191be17a766f3910c02db6c3ed
|
||||
README.md: b75969640e968bb251cd073caf7f3cf6719179fc
|
||||
README.zh.md: 5ed0988d9624b948cfeecedf086e2e5d1f714b60
|
||||
|
||||
@@ -14,7 +14,7 @@ The mux stream projects the latest log-backed title as a validated `session/titl
|
||||
|
||||
Session model routing is a session-domain contract. `session.models` returns the selected provider/model/reasoning target with provider-grouped advisory models, exact-route reasoning metadata, and provider-local lookup failures. `session.selectModel` validates the optional adapter-owned reasoning effort and replaces the complete target selected for the next prompt-assembly boundary. Catalog membership is not validation: an adapter may resolve an unlisted model, while an unavailable route or unsupported effort returns `model-unavailable`.
|
||||
|
||||
Workspace and Session lists are separate reconnect baselines. `workspace.create` creates a unique name or adopts an existing directory, `session.create` accepts an optional preallocated Session id, and `host/workspace-changed` plus `host/session-added` carry committed increments in either arrival order. `SessionSummary.blank` and the `host/session-added` frame carry the derived zero-events bit: clients hide blank sessions and reuse them per workspace, flip blank on the first `host/session-status(running:true)`, and treat `session.list` as the reconnect authority; cold summaries are never blank because lazy persistence keeps never-appended sessions out of `list()`.
|
||||
Workspace and Session lists are separate reconnect baselines. `workspace.create` creates a unique name or adopts an existing directory, `workspace.delete` removes only the Workspace registration, `session.create` accepts an optional preallocated Session id, and `host/workspace-changed`, `host/workspace-removed`, plus `host/session-added` carry committed increments in either arrival order. Registration deletion preserves the directory and session logs; its Sessions remain in `session.list` and become Ungrouped. `SessionSummary.blank` and the `host/session-added` frame carry the derived zero-events bit: clients hide blank sessions and reuse them per workspace, flip blank on the first `host/session-status(running:true)`, and treat `session.list` as the reconnect authority; cold summaries are never blank because lazy persistence keeps never-appended sessions out of `list()`.
|
||||
|
||||
`session.history` pages on message boundaries, and its tail page (no `beforeSeq`) carries two session-level extras the page window cannot supply: the in-flight partial's chunk events, and `todos` — the latest `todo/write` whole-list projection over the full log. Older pages omit `todos` because the projection is session-level, not per-page; a tail response that omits it means the whole log holds no `todo/write`, so clients read the absent field as the empty plan rather than as unchanged state.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ mux 流会在每个已附加会话的订阅基线之后,以及对应的实时
|
||||
|
||||
会话模型路由属于会话领域契约。`session.models` 返回选中的提供方/模型/推理(reasoning)目标,以及按提供方分组的建议性模型、精确路由推理元数据和逐提供方查询失败记录。`session.selectModel` 校验由适配器持有的可选推理强度,并替换将在下一提示词组装边界使用的完整目标。目录成员关系不构成校验:适配器可以解析未列出的模型,而不可用路由或不受支持的推理强度会返回 `model-unavailable`。
|
||||
|
||||
Workspace 列表与 Session 列表是相互独立的重连基线。`workspace.create` 会创建唯一名称或接纳现有目录,`session.create` 接受可选的预分配 Session id,`host/workspace-changed` 与 `host/session-added` 则以任意到达顺序携带已提交的增量。`SessionSummary.blank` 与 `host/session-added` 帧携带派生的零事件位:客户端隐藏空白会话并按 workspace 复用它们,在首个 `host/session-status(running:true)` 时翻转 blank,并以 `session.list` 作为重连权威;冷会话摘要永远不是空白——惰性持久化让从未追加过事件的会话根本不出现在 `list()` 中。
|
||||
Workspace 列表与 Session 列表是相互独立的重连基线。`workspace.create` 会创建唯一名称或接纳现有目录,`workspace.delete` 只移除 Workspace 注册记录,`session.create` 接受可选的预分配 Session id,`host/workspace-changed`、`host/workspace-removed` 与 `host/session-added` 则以任意到达顺序携带已提交的增量。删除注册记录会保留目录和会话日志;相关 Session 仍留在 `session.list` 中,并进入 Ungrouped。`SessionSummary.blank` 与 `host/session-added` 帧携带派生的零事件位:客户端隐藏空白会话并按 workspace 复用它们,在首个 `host/session-status(running:true)` 时翻转 blank,并以 `session.list` 作为重连权威;冷会话摘要永远不是空白:惰性持久化让从未追加过事件的会话根本不出现在 `list()` 中。
|
||||
|
||||
`session.history` 按消息边界分页,其尾页(不带 `beforeSeq`)额外携带两项页窗口本身无法提供的会话级数据:进行中局部消息的 chunk 事件,以及 `todos`——整份日志上最后一次 `todo/write` 的整表投影。较早的页面不带 `todos`,因为该投影是会话级而非分页级的;尾页响应缺少该字段意味着整份日志中没有任何 `todo/write`,因此客户端要把缺失字段读作空计划,而不是读作「状态未变」。
|
||||
|
||||
|
||||
@@ -932,6 +932,15 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
return ok(request, { workspace: workspaceView(workspace) })
|
||||
},
|
||||
|
||||
async delete(request) {
|
||||
const { workspaceId } = request.payload
|
||||
const operation = workspaceCreationChain.then(() =>
|
||||
ctx.workspace.delete(brandWorkspaceId(workspaceId)))
|
||||
workspaceCreationChain = operation.then(() => undefined, () => undefined)
|
||||
if (!await operation) return workspaceNotFound(request, workspaceId)
|
||||
return ok(request, { deleted: true as const })
|
||||
},
|
||||
|
||||
async insertSessionBefore(request) {
|
||||
const { payload } = request
|
||||
const workspace = ctx.workspace.get(brandWorkspaceId(payload.workspaceId))
|
||||
@@ -1145,8 +1154,9 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
queue.push(frame({ type: 'host/agent-error', sessionId: agent.id, message: String(error) }))
|
||||
}),
|
||||
ctx.on('domain/changed', (change) => {
|
||||
if (change.domain !== 'workspace' || change.operation !== 'put') return
|
||||
if (change.domain !== 'workspace') return
|
||||
if (change.table === '') {
|
||||
if (change.operation !== 'put') return
|
||||
const state = workspaceDomainState.parse(change.value)
|
||||
for (const workspaceId of state.workspaceIds) {
|
||||
if (committedWorkspaceIds.has(workspaceId)) continue
|
||||
@@ -1159,7 +1169,16 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
}
|
||||
return
|
||||
}
|
||||
if (change.table !== 'workspaces' || !committedWorkspaceIds.has(change.key)) return
|
||||
if (change.table !== 'workspaces') return
|
||||
if (change.operation === 'deleted') {
|
||||
if (!committedWorkspaceIds.delete(change.key)) return
|
||||
queue.push(frame({
|
||||
type: 'host/workspace-removed',
|
||||
workspaceId: change.key as WorkspaceId,
|
||||
}))
|
||||
return
|
||||
}
|
||||
if (!committedWorkspaceIds.has(change.key)) return
|
||||
// Existing-entity table writes are complete attach/touch commits.
|
||||
// A new entity's first put waits for the global registry write above.
|
||||
queue.push(frame({
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { Wire } from './rpc.schema.ts'
|
||||
import { rpcErrorSchema, rpcIdSchema } from './rpc.schema.ts'
|
||||
import { approvalRequestIdSchema } from './approvals.schema.ts'
|
||||
import { contentBlockSchema, sessionEventSchema, sessionIdSchema, toolEventViewSchema } from './sessions.schema.ts'
|
||||
import { workspaceViewSchema } from './workspace.schema.ts'
|
||||
import { workspaceIdSchema, workspaceViewSchema } from './workspace.schema.ts'
|
||||
|
||||
/** Question shape validated strictly against core dsh-user-interaction. */
|
||||
export const askUserQuestionItemSchema = z.object({
|
||||
@@ -47,6 +47,7 @@ export const hostFrameSchema = z.discriminatedUnion('type', [
|
||||
z.object({ type: z.literal('host/session-status'), sessionId: sessionIdSchema, running: z.boolean() }),
|
||||
z.object({ type: z.literal('host/agent-error'), sessionId: sessionIdSchema, message: z.string() }),
|
||||
z.object({ type: z.literal('host/workspace-changed'), workspace: workspaceViewSchema }),
|
||||
z.object({ type: z.literal('host/workspace-removed'), workspaceId: workspaceIdSchema }),
|
||||
z.object({ type: z.literal('host/commands-changed') }),
|
||||
z.object({ type: z.literal('stream/error'), error: rpcErrorSchema }),
|
||||
]) as unknown as z.ZodType<HostFrame>
|
||||
|
||||
@@ -85,7 +85,9 @@ export type MuxFrame =
|
||||
* agent-error is the only outlet for live failures with no turn position;
|
||||
* workspace-changed pushes the full new snapshot after every durable
|
||||
* workspace mutation (create/attach/order change — the client upserts, while
|
||||
* `workspace.list` provides the reconnect baseline).
|
||||
* `workspace.list` provides the reconnect baseline); workspace-removed is the
|
||||
* committed registration-deletion increment and never implies directory or
|
||||
* session-log deletion.
|
||||
*/
|
||||
export type HostFrame =
|
||||
| { type: 'host/session-added'; sessionId: SessionId; blank: boolean; parentSessionId?: SessionId; cwd?: string }
|
||||
@@ -93,6 +95,7 @@ export type HostFrame =
|
||||
| { type: 'host/session-status'; sessionId: SessionId; running: boolean }
|
||||
| { type: 'host/agent-error'; sessionId: SessionId; message: string }
|
||||
| { type: 'host/workspace-changed'; workspace: WorkspaceView }
|
||||
| { type: 'host/workspace-removed'; workspaceId: WorkspaceView['workspaceId'] }
|
||||
/**
|
||||
* The command registry changed (`commands/change` passthrough). Pure
|
||||
* invalidation signal, no payload: clients refetch `command.list` in the
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface RpcMethodMap {
|
||||
'workspace.list': WorkspaceApi['list']
|
||||
'workspace.create': WorkspaceApi['create']
|
||||
'workspace.rename': WorkspaceApi['rename']
|
||||
'workspace.delete': WorkspaceApi['delete']
|
||||
'workspace.insertSessionBefore': WorkspaceApi['insertSessionBefore']
|
||||
'command.list': CommandsApi['list']
|
||||
'command.execute': CommandsApi['execute']
|
||||
|
||||
@@ -59,6 +59,16 @@ export const workspaceRenameValueSchema = z.object({
|
||||
workspace: workspaceViewSchema,
|
||||
}) satisfies z.ZodType<Wire<ResponseValue<'workspace.rename'>>>
|
||||
|
||||
/** workspace.delete request payload. */
|
||||
export const workspaceDeleteRequestSchema = z.object({
|
||||
workspaceId: workspaceIdSchema,
|
||||
}) satisfies z.ZodType<Wire<RequestPayload<'workspace.delete'>>>
|
||||
|
||||
/** workspace.delete response value. */
|
||||
export const workspaceDeleteValueSchema = z.object({
|
||||
deleted: z.literal(true),
|
||||
}) satisfies z.ZodType<Wire<ResponseValue<'workspace.delete'>>>
|
||||
|
||||
/** workspace.insertSessionBefore request payload (anchor omitted = append to end). */
|
||||
export const workspaceInsertSessionBeforeRequestSchema = z.object({
|
||||
workspaceId: workspaceIdSchema,
|
||||
|
||||
@@ -65,6 +65,14 @@ export interface WorkspaceApi {
|
||||
rename(request: RpcRequest<{ workspaceId: WorkspaceId; title: string }>):
|
||||
Promise<RpcResponse<{ workspace: WorkspaceView }>>
|
||||
|
||||
/**
|
||||
* Removes one Workspace registration. The directory, every user file, and
|
||||
* every session log remain untouched; those Sessions consequently become
|
||||
* ungrouped. An unknown id fails with `workspace-not-found`.
|
||||
*/
|
||||
delete(request: RpcRequest<{ workspaceId: WorkspaceId }>):
|
||||
Promise<RpcResponse<{ deleted: true }>>
|
||||
|
||||
/**
|
||||
* Moves an accounted session within its workspace's manual order,
|
||||
* DOM-insertBefore-like: with `beforeSessionId` the session is inserted
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from '../api/sessions.schema.ts'
|
||||
import {
|
||||
workspaceCreateValueSchema,
|
||||
workspaceDeleteValueSchema,
|
||||
workspaceInsertSessionBeforeValueSchema,
|
||||
workspaceListValueSchema,
|
||||
workspaceRenameValueSchema,
|
||||
@@ -64,6 +65,7 @@ export interface IApiClient {
|
||||
list(payload: RequestPayload<'workspace.list'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'workspace.list'>>>
|
||||
create(payload: RequestPayload<'workspace.create'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'workspace.create'>>>
|
||||
rename(payload: RequestPayload<'workspace.rename'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'workspace.rename'>>>
|
||||
delete(payload: RequestPayload<'workspace.delete'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'workspace.delete'>>>
|
||||
insertSessionBefore(payload: RequestPayload<'workspace.insertSessionBefore'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'workspace.insertSessionBefore'>>>
|
||||
}
|
||||
commands: {
|
||||
@@ -97,6 +99,7 @@ const UNARY_VALUE_SCHEMAS: { [K in keyof RpcMethodMap]: z.ZodType<Wire<ResponseV
|
||||
'workspace.list': workspaceListValueSchema,
|
||||
'workspace.create': workspaceCreateValueSchema,
|
||||
'workspace.rename': workspaceRenameValueSchema,
|
||||
'workspace.delete': workspaceDeleteValueSchema,
|
||||
'workspace.insertSessionBefore': workspaceInsertSessionBeforeValueSchema,
|
||||
'command.list': commandListValueSchema,
|
||||
'command.execute': commandExecuteValueSchema,
|
||||
@@ -293,6 +296,7 @@ export abstract class AbstractApiClient implements IApiClient {
|
||||
list: (payload, signal) => this.callUnary('workspace.list', payload, signal),
|
||||
create: (payload, signal) => this.callUnary('workspace.create', payload, signal),
|
||||
rename: (payload, signal) => this.callUnary('workspace.rename', payload, signal),
|
||||
delete: (payload, signal) => this.callUnary('workspace.delete', payload, signal),
|
||||
insertSessionBefore: (payload, signal) => this.callUnary('workspace.insertSessionBefore', payload, signal),
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { hostDescribeRequestSchema } from '../api/host.schema.ts'
|
||||
import {
|
||||
workspaceCreateRequestSchema,
|
||||
workspaceDeleteRequestSchema,
|
||||
workspaceInsertSessionBeforeRequestSchema,
|
||||
workspaceListRequestSchema,
|
||||
workspaceRenameRequestSchema,
|
||||
@@ -61,6 +62,7 @@ const UNARY_ROUTES: UnaryRoutes = {
|
||||
'workspace.list': { schema: workspaceListRequestSchema, invoke: (api, r) => api.workspace.list(r) },
|
||||
'workspace.create': { schema: workspaceCreateRequestSchema, invoke: (api, r) => api.workspace.create(r) },
|
||||
'workspace.rename': { schema: workspaceRenameRequestSchema, invoke: (api, r) => api.workspace.rename(r) },
|
||||
'workspace.delete': { schema: workspaceDeleteRequestSchema, invoke: (api, r) => api.workspace.delete(r) },
|
||||
'workspace.insertSessionBefore': { schema: workspaceInsertSessionBeforeRequestSchema, invoke: (api, r) => api.workspace.insertSessionBefore(r) },
|
||||
'command.list': { schema: commandListRequestSchema, invoke: (api, r) => api.commands.list(r) },
|
||||
'command.execute': { schema: commandExecuteRequestSchema, invoke: (api, r, signal) => api.commands.execute(r, signal) },
|
||||
|
||||
@@ -244,4 +244,37 @@ describe('Host Workspace increments', () => {
|
||||
abort.abort()
|
||||
expect(await next).toMatchObject({ done: true })
|
||||
})
|
||||
|
||||
it('deletes the registration, keeps its session and folder, and streams one removal', async () => {
|
||||
const { api, ctx } = await harness()
|
||||
const workspace = expectOk(await api.workspace.create(request({ name: 'delete-me' }))).workspace
|
||||
const sessionId = SessionId('session-kept-after-workspace-delete')
|
||||
expectOk(await api.sessions.create(request({ workspaceId: workspace.workspaceId, sessionId })))
|
||||
|
||||
const abort = new AbortController()
|
||||
const stream: AsyncIterator<RpcRequest<HostFrame>> =
|
||||
api.events.host(request({}), abort.signal)[Symbol.asyncIterator]()
|
||||
const removed = nextHostFrame(stream)
|
||||
expectOk(await api.workspace.delete(request({ workspaceId: workspace.workspaceId })))
|
||||
expect(await removed).toMatchObject({
|
||||
payload: { type: 'host/workspace-removed', workspaceId: workspace.workspaceId },
|
||||
})
|
||||
expect(expectOk(await api.workspace.list(request({}))).items).toEqual([])
|
||||
expect(expectOk(await api.sessions.list(request({}))).items.map(item => item.sessionId)).toContain(sessionId)
|
||||
expect(ctx.agents.get(sessionId)).toBeDefined()
|
||||
expect(existsSync(workspace.path)).toBe(true)
|
||||
|
||||
const missing = await api.workspace.delete(request({ workspaceId: workspace.workspaceId }))
|
||||
expect(missing.result).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: 'workspace-not-found', details: { workspaceId: workspace.workspaceId } },
|
||||
})
|
||||
|
||||
const reregistered = expectOk(await api.workspace.create(request({ path: workspace.path }))).workspace
|
||||
expect(reregistered.workspaceId).not.toBe(workspace.workspaceId)
|
||||
expect(reregistered.path).toBe(workspace.path)
|
||||
expect(reregistered.sessionIds).toEqual([])
|
||||
expect(expectOk(await api.sessions.list(request({}))).items.map(item => item.sessionId)).toContain(sessionId)
|
||||
abort.abort()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -52,6 +52,7 @@ function scriptedApi(overrides: {
|
||||
list: r => ok(r, { items: [] }),
|
||||
create: r => ok(r, { workspace: { workspaceId: 'w1' as never, path: '/t', title: 't', sessionIds: [], createdAt: '0', updatedAt: '0' }, created: true }),
|
||||
rename: r => ok(r, { workspace: { workspaceId: 'w1' as never, path: '/t', title: 't', sessionIds: [], createdAt: '0', updatedAt: '0' } }),
|
||||
delete: r => ok(r, { deleted: true as const }),
|
||||
insertSessionBefore: r => ok(r, { workspace: { workspaceId: 'w1' as never, path: '/t', title: 't', sessionIds: [], createdAt: '0', updatedAt: '0' } }),
|
||||
},
|
||||
commands: {
|
||||
@@ -88,13 +89,15 @@ describe('unary round trip', () => {
|
||||
expect(response.result).toEqual({ ok: true, value: { items: [{ sessionId: 's1', updatedAt: 7, running: false, blank: false }] } })
|
||||
})
|
||||
|
||||
it('routes workspace rename and insertSessionBefore through the wire', async () => {
|
||||
it('routes workspace rename, delete, and insertSessionBefore through the wire', async () => {
|
||||
const api = scriptedApi()
|
||||
const c = client(api)
|
||||
const renamed = await c.workspace.rename({ workspaceId: 'w1' as never, title: 'next' })
|
||||
expect(renamed.result.ok).toBe(true)
|
||||
const blankTitle = await c.workspace.rename({ workspaceId: 'w1' as never, title: ' ' })
|
||||
expect(blankTitle.result).toMatchObject({ ok: false, error: { code: 'bad-request' } })
|
||||
const deleted = await c.workspace.delete({ workspaceId: 'w1' as never })
|
||||
expect(deleted.result).toEqual({ ok: true, value: { deleted: true } })
|
||||
const anchored = await c.workspace.insertSessionBefore({ workspaceId: 'w1' as never, sessionId: sid('s1'), beforeSessionId: sid('s2') })
|
||||
expect(anchored.result.ok).toBe(true)
|
||||
const appended = await c.workspace.insertSessionBefore({ workspaceId: 'w1' as never, sessionId: sid('s1') })
|
||||
|
||||
@@ -94,6 +94,9 @@ function fakeApi(overrides: Partial<{ muxFrames: MuxFrame[]; hostFrames: HostFra
|
||||
result: { ok: true, value: { workspace: { workspaceId: 'w1' as never, path: '/w', title: 'w', sessionIds: [], createdAt: 't', updatedAt: 't' } } },
|
||||
}
|
||||
},
|
||||
async delete(request) {
|
||||
return { rpcId: request.rpcId, result: { ok: true, value: { deleted: true as const } } }
|
||||
},
|
||||
async insertSessionBefore(request) {
|
||||
return {
|
||||
rpcId: request.rpcId,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { hostDescribeRequestSchema, hostDescribeValueSchema } from '../src/api/host.schema.ts'
|
||||
import {
|
||||
workspaceCreateRequestSchema, workspaceCreateValueSchema, workspaceIdSchema,
|
||||
workspaceDeleteRequestSchema, workspaceDeleteValueSchema,
|
||||
workspaceInsertSessionBeforeRequestSchema, workspaceInsertSessionBeforeValueSchema,
|
||||
workspaceListRequestSchema, workspaceListValueSchema,
|
||||
workspaceRenameRequestSchema, workspaceRenameValueSchema, workspaceViewSchema,
|
||||
@@ -238,6 +239,13 @@ describe('workspace domain schemas', () => {
|
||||
expect(workspaceRenameValueSchema.parse({ workspace: view }).workspace.workspaceId).toBe('w1')
|
||||
})
|
||||
|
||||
it('validates workspace deletion payload and receipt', () => {
|
||||
expect(workspaceDeleteRequestSchema.parse({ workspaceId: 'w1' }).workspaceId).toBe('w1')
|
||||
expect(() => workspaceDeleteRequestSchema.parse({})).toThrow()
|
||||
expect(workspaceDeleteValueSchema.parse({ deleted: true })).toEqual({ deleted: true })
|
||||
expect(() => workspaceDeleteValueSchema.parse({ deleted: false })).toThrow()
|
||||
})
|
||||
|
||||
it('insertSessionBefore accepts an anchored and an anchorless move', () => {
|
||||
expect(workspaceInsertSessionBeforeRequestSchema.parse({ workspaceId: 'w1', sessionId: 's1', beforeSessionId: 's2' }).beforeSessionId).toBe('s2')
|
||||
expect(workspaceInsertSessionBeforeRequestSchema.parse({ workspaceId: 'w1', sessionId: 's1' }).beforeSessionId).toBeUndefined()
|
||||
@@ -334,6 +342,11 @@ describe('events frame schemas', () => {
|
||||
{ type: 'host/session-removed', sessionId: 's' },
|
||||
{ type: 'host/session-status', sessionId: 's', running: true },
|
||||
{ type: 'host/agent-error', sessionId: 's', message: 'boom' },
|
||||
{ type: 'host/workspace-changed', workspace: {
|
||||
workspaceId: 'w', path: '/w', title: 'w', sessionIds: [],
|
||||
createdAt: '0', updatedAt: '0',
|
||||
} },
|
||||
{ type: 'host/workspace-removed', workspaceId: 'w' },
|
||||
{ type: 'host/commands-changed' },
|
||||
{ type: 'stream/error', error: { code: 'internal', message: 'm', details: {} } },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user