From 1acbf46d5344ae35ef719ad391e846964cc186cd Mon Sep 17 00:00:00 2001 From: _Kerman Date: Wed, 21 Oct 2026 21:36:49 +0800 Subject: [PATCH] fix(ui-workspace): preserve explicit navigation after startup failure --- packages/client/ui-workspace/README.i18n.yaml | 4 +-- packages/client/ui-workspace/README.md | 2 +- packages/client/ui-workspace/README.zh.md | 2 +- .../ui-workspace/src/client/navigation.ts | 4 ++- .../tests/workspaces-service.client.spec.ts | 35 +++++++++++++++++++ 5 files changed, 42 insertions(+), 5 deletions(-) diff --git a/packages/client/ui-workspace/README.i18n.yaml b/packages/client/ui-workspace/README.i18n.yaml index 6c301a51fd..626e8c4bf9 100644 --- a/packages/client/ui-workspace/README.i18n.yaml +++ b/packages/client/ui-workspace/README.i18n.yaml @@ -33,8 +33,8 @@ en: 9fb88260380bf093 zh: 3bace2b96ecd4a82 /deepseek-ai-dsh-client-ui-workspace/use-this-package/active-schedule-markers: - en: 57f47add55b9f5c2 - zh: af7fe7f6b32cd30a + en: ac15c05cb045f748 + zh: 6330172c7231b2b8 /deepseek-ai-dsh-client-ui-workspace/understand-the-implementation: en: 96a37e74555a36b7 zh: d03941523509a5cf diff --git a/packages/client/ui-workspace/README.md b/packages/client/ui-workspace/README.md index 1bf96818b6..eaa9add9d4 100644 --- a/packages/client/ui-workspace/README.md +++ b/packages/client/ui-workspace/README.md @@ -77,7 +77,7 @@ Grouped and flat Session rows show a clock mark when the Session has active sche Navigation and startup restoration obtain the selected Session's projections from its `follow`; neither issues a separate projection refresh for that Session or its parent, except that restoring a saved blank first reloads that Session's projections, because listed values are cached hints that can lag its log after an unclean Host exit. -New Session, Workspace reopening, and explicit draft preparation name the first eligible blank in catalog order. A request that names a preset skips a blank whose listed preset is a different one, so a preset flow such as the creator entry does not take over another preset's draft; a request without a preset may name any eligible blank, which the Host switches to its default. A reused blank may keep the preset composition it was created with. Startup restoration names the saved blank. The Host reuses the named blank under the requested preset, or under its default when the request names none, and creates another Session when the blank has started, left the Workspace, or is held by another writer; navigation opens the returned Session without trying other blanks. `startSession` with draft options, `openWorkspace`, and `connectWorkspace` accept `{ agentPreset }` for a flow that knows its preset before it starts, such as the creator entry; a connection joins the latest in-flight connection to its Workspace only when both request the same preset, so the latest request always reaches the Host. Startup restoration passes the preset the saved blank's log recorded, so the draft keeps that preset; when the Host reports the preset as missing (`agent-preset/not-found`) or failing to activate (`agent-preset/invalid`), restoration asks again without a preset and the draft continues under the Host default. Other failures abort the request: an explicit New Session or hero Workspace pick shows the failure as a transient notice quoting the Host's code and message (for example a preset that fails to mount) or, for a failure that is not a Host refusal, that failure's own message; a request a later navigation or owner disposal superseded raises no notice, and startup restoration reports only to the console. Released blanks retain their slash-command state when reused. Later navigation cancels a pending startup selection. +New Session, Workspace reopening, and explicit draft preparation name the first eligible blank in catalog order. A request that names a preset skips a blank whose listed preset is a different one, so a preset flow such as the creator entry does not take over another preset's draft; a request without a preset may name any eligible blank, which the Host switches to its default. A reused blank may keep the preset composition it was created with. Startup restoration names the saved blank. The Host reuses the named blank under the requested preset, or under its default when the request names none, and creates another Session when the blank has started, left the Workspace, or is held by another writer; navigation opens the returned Session without trying other blanks. `startSession` with draft options, `openWorkspace`, and `connectWorkspace` accept `{ agentPreset }` for a flow that knows its preset before it starts, such as the creator entry; a connection joins the latest in-flight connection to its Workspace only when both request the same preset, so the latest request always reaches the Host. Startup restoration passes the preset the saved blank's log recorded, so the draft keeps that preset; when the Host reports the preset as missing (`agent-preset/not-found`) or failing to activate (`agent-preset/invalid`), restoration asks again without a preset and the draft continues under the Host default. Other failures abort the request: an explicit New Session or hero Workspace pick shows the failure as a transient notice quoting the Host's code and message (for example a preset that fails to mount) or, for a failure that is not a Host refusal, that failure's own message; a request a later navigation or owner disposal superseded raises no notice, and startup restoration reports only to the console. Released blanks retain their slash-command state when reused. Later navigation cancels a pending startup selection and prevents failed restoration from retrying on later catalog updates. Once both Workspace and Session startup baselines are ready, an empty installation calls `workspaces.initializeDefault` and creates or reuses its blank Session. The composer becomes editable when that Session is selected; no message is submitted automatically. Later navigation or owner disposal prevents startup from selecting its result. Ineligible first use leaves the folder picker available without an error. Default Workspace creation failure shows a transient notice directing the user to Choose workspace, and is not retried until the next startup. Session creation failures use the ordinary restoration error handling. The registered Workspace remains available if Session creation or later submission fails. diff --git a/packages/client/ui-workspace/README.zh.md b/packages/client/ui-workspace/README.zh.md index dbeb6a6e35..cc7e13b7c7 100644 --- a/packages/client/ui-workspace/README.zh.md +++ b/packages/client/ui-workspace/README.zh.md @@ -77,7 +77,7 @@ Session 行渲染运行时的实时 `pendingInteraction` 分类:审批显示** 导航和启动恢复通过所选 Session 的 `follow` 获取投影,不会另行刷新该 Session 或其父会话的投影;例外是恢复已保存的空白会话时先重新加载该 Session 的投影,因为列表中的值是缓存提示,Host 非正常退出后可能落后于其日志。 -新会话、重新打开工作区和显式准备草稿向 Host 请求 Session 时,指明列表中第一个符合条件的空白会话。指明 preset 的请求会跳过列出其他 preset 的空白会话,因此创造模式入口等 preset 流程不会接管其他 preset 的草稿;未指明 preset 的请求可以指明任何符合条件的空白会话,由 Host 切换到其默认 preset。被复用的空白会话可能沿用创建时的 preset 装配。启动恢复指明已保存的空白会话。Host 以请求的 preset 复用该空白会话,请求未指明 preset 时使用其默认 preset;若该空白会话已开始、已离开该工作区或写锁被其他写者占用,Host 改为新建 Session;导航打开 Host 返回的 Session,不再尝试其他空白会话。带草稿选项的 `startSession`、`openWorkspace` 和 `connectWorkspace` 接受 `{ agentPreset }`,供开始前已确定 preset 的流程使用,例如创造模式入口;一次连接仅在与该工作区最近一次进行中的连接请求相同 preset 时与之合并,因此最新的请求总会到达 Host。启动恢复传入已保存空白会话日志记录的 preset,使草稿保留该 preset;当 Host 报告该 preset 不存在(`agent-preset/not-found`)或无法激活(`agent-preset/invalid`)时,启动恢复改为不带 preset 再请求一次,草稿在 Host 默认 preset 下继续。其他错误会中止请求:显式新建会话或在 hero 中选择工作区时,失败以短暂提示展示,引用 Host 的错误码和消息(例如 preset 挂载失败),非 Host 拒绝的失败则显示其自身消息;已被后续导航或 owner 销毁取代的请求不弹提示,启动恢复仍只报告到控制台。已释放的空白会话被复用时保留 slash 命令状态。后续导航会取消尚未完成的启动选择。 +新会话、重新打开工作区和显式准备草稿向 Host 请求 Session 时,指明列表中第一个符合条件的空白会话。指明 preset 的请求会跳过列出其他 preset 的空白会话,因此创造模式入口等 preset 流程不会接管其他 preset 的草稿;未指明 preset 的请求可以指明任何符合条件的空白会话,由 Host 切换到其默认 preset。被复用的空白会话可能沿用创建时的 preset 装配。启动恢复指明已保存的空白会话。Host 以请求的 preset 复用该空白会话,请求未指明 preset 时使用其默认 preset;若该空白会话已开始、已离开该工作区或写锁被其他写者占用,Host 改为新建 Session;导航打开 Host 返回的 Session,不再尝试其他空白会话。带草稿选项的 `startSession`、`openWorkspace` 和 `connectWorkspace` 接受 `{ agentPreset }`,供开始前已确定 preset 的流程使用,例如创造模式入口;一次连接仅在与该工作区最近一次进行中的连接请求相同 preset 时与之合并,因此最新的请求总会到达 Host。启动恢复传入已保存空白会话日志记录的 preset,使草稿保留该 preset;当 Host 报告该 preset 不存在(`agent-preset/not-found`)或无法激活(`agent-preset/invalid`)时,启动恢复改为不带 preset 再请求一次,草稿在 Host 默认 preset 下继续。其他错误会中止请求:显式新建会话或在 hero 中选择工作区时,失败以短暂提示展示,引用 Host 的错误码和消息(例如 preset 挂载失败),非 Host 拒绝的失败则显示其自身消息;已被后续导航或 owner 销毁取代的请求不弹提示,启动恢复仍只报告到控制台。已释放的空白会话被复用时保留 slash 命令状态。后续导航会取消尚未完成的启动选择,并阻止失败的恢复在后续列表更新时重试。 Workspace 和 Session 的启动基线均就绪后,空安装环境调用 `workspaces.initializeDefault`,创建或复用其空白 Session。选中该 Session 后输入框才可编辑,不会自动提交消息。后续导航或所属上下文销毁会阻止启动流程选中其结果。不符合首次使用条件时仍可选择文件夹,不显示错误。默认工作区创建失败时显示短暂提示,引导用户通过“选择工作区”选择文件夹,直到下次启动才重试。Session 创建失败沿用普通的恢复错误处理。登记成功的工作区在 Session 创建或后续提交失败时仍然保留。 diff --git a/packages/client/ui-workspace/src/client/navigation.ts b/packages/client/ui-workspace/src/client/navigation.ts index b4db2dd32e..6b4221ae19 100644 --- a/packages/client/ui-workspace/src/client/navigation.ts +++ b/packages/client/ui-workspace/src/client/navigation.ts @@ -173,6 +173,7 @@ class UiWorkspaceService extends Service implements UiWorkspace { {}, { persist: { name: 'dsh.sessions.current' } }, ) private mainReference: SessionReference | undefined + private restorationNavigation: AbortSignal | undefined /** * @param ctx - Client root Context. @@ -431,7 +432,7 @@ class UiWorkspaceService extends Service implements UiWorkspace { const reconcile = (): void => { if (this.lifetime.signal.aborted) return if (this.clearArchivedCurrent()) return - if (initial !== 'waiting') return + if (initial !== 'waiting' || this.restorationNavigation?.aborted === true) return const workspace = this.workspaces.list.getSnapshot() const sessions = this.sessions.list.getSnapshot() if (workspace.phase !== 'ready' || sessions.phase !== 'ready') return @@ -474,6 +475,7 @@ class UiWorkspaceService extends Service implements UiWorkspace { return } const navigation = AbortSignal.any([this.ctx.layout.beginNavigation(), this.lifetime.signal]) + this.restorationNavigation = navigation let sessionId: SessionId | undefined if (summary !== undefined && workspace !== undefined && summary.cwd === workspace.path && !workspaces.archivedSessionIds.includes(summary.id)) { diff --git a/packages/client/ui-workspace/tests/workspaces-service.client.spec.ts b/packages/client/ui-workspace/tests/workspaces-service.client.spec.ts index 90d3e8e0b9..f4e5bdcac9 100644 --- a/packages/client/ui-workspace/tests/workspaces-service.client.spec.ts +++ b/packages/client/ui-workspace/tests/workspaces-service.client.spec.ts @@ -1341,6 +1341,41 @@ describe('UiWorkspaceService', () => { }) }) + it('keeps an explicit creation failure visible after a failed startup and a catalog update', async () => { + const startupFailure = new Error('startup failed') + const warning = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + const creating = Promise.withResolvers() + const created = Promise.withResolvers() + const b = bench({ + sessions: sessionState(), + workspaces: workspaceState([workspace('a')]), + configureSessions: (sessions) => { + sessions.create.mockRejectedValueOnce(startupFailure).mockImplementationOnce(() => { + creating.resolve(undefined) + return created.promise + }) + }, + }) + await vi.waitFor(() => { + expect(warning).toHaveBeenCalledExactlyOnceWith('initial Session restoration failed:', startupFailure) + }) + + b.uiWorkspace.startSession(wid('a')) + await creating.promise + b.sessions.list.set(sessionState([summary('creating', { blank: true, cwd: '/w/a' })])) + created.reject(new Error('creation refused')) + + await vi.waitFor(() => { + expect(b.notify).toHaveBeenCalledExactlyOnceWith({ kind: 'createFailed', message: 'creation refused' }) + }) + expect(b.sessions.create).toHaveBeenCalledTimes(2) + expect(b.sessions.retain).not.toHaveBeenCalled() + expect(warning.mock.calls).toEqual([ + ['initial Session restoration failed:', startupFailure], + ['new session failed:', expect.any(Error)], + ]) + }) + it('settles empty startup without creating a Session on later catalog updates', () => { const b = bench({ sessions: sessionState(), workspaces: workspaceState() }) b.workspaces.list.set(workspaceState([workspace('a')]))