refactor(workspace): rebuild runtime bundle across /cwd changes#216
refactor(workspace): rebuild runtime bundle across /cwd changes#216fennoai[bot] wants to merge 7 commits into
Conversation
Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: Yumiue <188874804+Yumiue@users.noreply.github.com>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
|
/code 补充测试 |
1 similar comment
|
/code 补充测试 |
| @@ -73,11 +92,14 @@ func Build(options Options) (Container, error) { | |||
| } | |||
There was a problem hiding this comment.
Build now forwards options.WorkspaceRoot/options.Workdir verbatim, but tui.New(...) still calls NewWithBootstrap(...) without setting either field. That leaves App.workspaceRoot empty for callers using the legacy constructor, so /cwd always looks like a cross-workspace switch and immediately fails with workspace rebuild is not configured. Please preserve the old constructor by defaulting these fields from the resolved config snapshot when the option is empty.
| if cmd := a.requestModelCatalogRefresh(a.state.CurrentProvider); cmd != nil { | ||
| cmds = append(cmds, cmd) | ||
| } | ||
| cmds = append(cmds, ListenForRuntimeEvent(a.runtime.Events())) |
There was a problem hiding this comment.
This starts a new runtime listener after every cross-workspace rebuild, but the old listener is never cancelled. Because ListenForRuntimeEventCmd blocks on the previous channel until it emits or closes, each rebuild leaves a goroutine waiting on the obsolete runtime, and any late event/close from that runtime can still mutate the new UI state. The listener needs an identity/cancellation mechanism when rebinding the app to a new runtime.
|
/simplify |
| a.state.ExecutionError = "" | ||
| a.state.StatusText = statusApplyingCommand | ||
| a.appendActivity("workspace", "Rebuilding workspace bundle", typed.Workdir, false) | ||
| cmds = append(cmds, runWorkspaceRebuild(a.rebuildWorkspace, typed.Notice, typed.Workdir)) |
There was a problem hiding this comment.
runWorkspaceRebuild is dispatched asynchronously, but the app never enters a distinct "rebuilding" busy state. Between this return and the later workspaceRebuildFinishedMsg, the user can still submit another prompt or trigger another /cwd, so the old runtime keeps accepting work and the eventual rebuild result then hard-resets the UI back to a fresh draft. This creates a real race/regression for cross-workspace switches. Please block input while a rebuild is in flight, or tag/cancel stale rebuild results before applying them.
| ConfigManager: options.ConfigManager, | ||
| Runtime: runtimeSvc, | ||
| ProviderService: providerSvc, | ||
| WorkspaceRoot: options.WorkspaceRoot, |
There was a problem hiding this comment.
Build now forwards WorkspaceRoot/Workdir/RebuildWorkspace exactly as provided, but the preserved tui.New(...) wrapper still constructs Options with only the old fields. In that path WorkspaceRoot stays empty and RebuildWorkspace is nil, so /cwd <path> is misclassified as a cross-workspace switch (SameRoot("", resolved.WorkspaceRoot) is always false) and then fails with workspace rebuild is not configured. Please derive sane defaults here or wire the compatibility wrapper so the legacy constructor still supports same-workspace /cwd.
| if configManager != nil { | ||
| workdir = strings.TrimSpace(configManager.Get().Workdir) | ||
| } | ||
| return NewWithWorkspace( |
There was a problem hiding this comment.
NewWithFactory is documented as falling back to the current config workdir for the workspace root, but it passes "" as workspaceRoot. Any caller that still uses this compatibility constructor skips the new root-boundary checks in resolveSessionWorkdir and tool execution, so it can move a session outside the original workspace instead of forcing a rebuild. Passing workdir here would preserve the previous single-workdir behavior and match the comment.
| target := strings.TrimSpace(requestedWorkdir) | ||
| if !filepath.IsAbs(target) { | ||
| target = filepath.Join(base, target) | ||
| if strings.TrimSpace(s.workspaceRoot) != "" && !isWithinWorkspaceRoot(s.workspaceRoot, resolved) { |
There was a problem hiding this comment.
isWithinWorkspaceRoot only does a lexical filepath.Rel check here. A repo-local symlink such as repo/link -> /tmp/outside still passes, so /cwd link keeps the old workspaceRoot instead of forcing a rebuild/reject. After that the TUI's file browser and local & commands run against the symlink target outside the workspace. This path needs canonicalization (or the same sandbox validation you use for tool execution) before accepting the new session workdir.
| @@ -73,11 +92,14 @@ func Build(options Options) (Container, error) { | |||
| } | |||
There was a problem hiding this comment.
This leaves WorkspaceRoot, Workdir, and RebuildWorkspace empty for callers that still use the exported tui.New(...) compatibility entry point. In that path workspaceRoot starts empty, so /cwd <path> is always treated as a cross-workspace switch and then fails with workspace rebuild is not configured. The compatibility wrapper needs to seed these fields from the existing config/runtime inputs, otherwise this PR breaks the documented old API.
| if cmd := a.requestModelCatalogRefresh(a.state.CurrentProvider); cmd != nil { | ||
| cmds = append(cmds, cmd) | ||
| } | ||
| cmds = append(cmds, ListenForRuntimeEvent(a.runtime.Events())) |
There was a problem hiding this comment.
After rebinding you start listening to the new runtime, but the pending ListenForRuntimeEvent command that was already blocked on the old runtime is never cancelled, and runtime.Service never closes its event channel. Rebuilding a few times will leak blocked listeners, and any late event from the old runtime can still mutate the new app state. This needs a generation guard or an explicit teardown path for the old subscription.
|
已补充测试并推送到当前 PR 分支,提交为 结果
验证
未做的事:没有直接查看外部 Codecov 结果,需要等 PR 上的新覆盖率回传。 |
Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: Yumiue <188874804+Yumiue@users.noreply.github.com>
Result
Validation
Commit
Not done:
|
Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: Yumiue <188874804+Yumiue@users.noreply.github.com>
|
/simplify |
Result
Verification
Commit
Not done:
|
Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: Yumiue <188874804+Yumiue@users.noreply.github.com>
|
/code 补上测试 |
结果
验证
提交
未做的事:没有改动任何生产代码,只补了这次 |
Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: Yumiue <188874804+Yumiue@users.noreply.github.com>
|
/code 解决测试错误 |
结果我先读取了仓库规则和这条 PR 的历史评论,再按当前分支状态直接复现测试。 本地未复现测试错误,当前 PR 分支在工作区里通过了以下验证:
补充确认:
未做的事:
|
|
/code 解决冲突 |
结果
验证
未做的事:
|
Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: Yumiue <188874804+Yumiue@users.noreply.github.com>
|
/code 修复以通过cli测试检查, 现在build_test错误,可能还有 |
结果
验证
未做
|
Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: Yumiue <188874804+Yumiue@users.noreply.github.com>
结果
已验证
结论
未做的事:
|
Requested by @Yumiue
This PR implements the
/cwdcross-workspace rebuild refactor from #214.Summary
internal/workspacefor unified path normalization and Git-root-awareworkspaceRootresolutionworkspaceRoot+workdir, and route same-workspace updates throughUpdateSessionWorkdir/cwd, reset to a clean draft, and refresh sessions/files/providers/modelsworkspaceRootwhile keeping executionworkdirindependentVerification
gofmt -w ./cmd ./internalgo test ./internal/tui/core/app ./internal/app ./internal/runtime ./internal/workspacego test ./...