mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: use per-chat plan file paths (#24268)
> This PR was authored by Mux on behalf of Mike. Chats sharing one workspace (e.g. sibling subagents) all wrote to `/home/coder/PLAN.md`, causing plan file collisions. This change derives a unique plan path per chat from the workspace home directory and chat ID. ## Changes * `write_file`, `edit_files`, and `propose_plan` reject any `plan.md` variant (case-insensitive) at the workspace home root, with a clear error pointing to the chat-specific path. * Root chats receive a `<plan-file-path>` block inlined in the main system prompt with the concrete path. * Prompt and tool descriptions no longer hardcode `/home/coder/PLAN.md`. * Plan path handling is POSIX-only (forward-slash), relying on the contract that workspace agent paths are normalized before reaching chatd. * Updated `ProposePlanTool.stories.tsx` to use per-chat path examples. * Full test coverage for plan path detection, legacy-path rejection in all three tools, inline prompt rendering, and fallback behavior.
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
package chattool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
||||
)
|
||||
|
||||
const planFileNamePrefix = "PLAN-"
|
||||
|
||||
// LegacySharedPlanPath is the original shared plan file path used by
|
||||
// every chat in a workspace.
|
||||
const LegacySharedPlanPath = "/home/coder/PLAN.md"
|
||||
|
||||
// ResolveWorkspaceHome returns the workspace user's home directory.
|
||||
func ResolveWorkspaceHome(
|
||||
ctx context.Context,
|
||||
conn workspacesdk.AgentConn,
|
||||
) (string, error) {
|
||||
if conn == nil {
|
||||
return "", xerrors.New("workspace connection is required")
|
||||
}
|
||||
|
||||
resp, err := conn.LS(ctx, "", workspacesdk.LSRequest{
|
||||
Path: []string{},
|
||||
Relativity: workspacesdk.LSRelativityHome,
|
||||
})
|
||||
if err != nil {
|
||||
return "", xerrors.Errorf("resolve workspace home: %w", err)
|
||||
}
|
||||
|
||||
home := strings.TrimSpace(resp.AbsolutePathString)
|
||||
if home == "" {
|
||||
return "", xerrors.New("workspace home path is empty")
|
||||
}
|
||||
|
||||
return home, nil
|
||||
}
|
||||
|
||||
// PlanPathForChat returns the per-chat plan file path rooted in the
|
||||
// workspace home directory.
|
||||
func PlanPathForChat(home string, chatID uuid.UUID) string {
|
||||
return path.Join(
|
||||
home,
|
||||
".coder",
|
||||
"plans",
|
||||
planFileNamePrefix+chatID.String()+".md",
|
||||
)
|
||||
}
|
||||
|
||||
// chatd consumes agent-normalized POSIX paths. Workspace agents are
|
||||
// expected to convert separators to forward slashes before these
|
||||
// helpers run.
|
||||
|
||||
// isAbsolutePath reports whether p is an absolute POSIX path.
|
||||
func isAbsolutePath(p string) bool {
|
||||
return path.IsAbs(p)
|
||||
}
|
||||
|
||||
// looksLikePlanFileName reports whether the base name of requestedPath
|
||||
// is "plan.md" (case-insensitive), ignoring the directory component.
|
||||
func looksLikePlanFileName(requestedPath string) bool {
|
||||
cleaned := path.Clean(requestedPath)
|
||||
return strings.EqualFold(path.Base(cleaned), "plan.md")
|
||||
}
|
||||
|
||||
// LooksLikeHomePlanFile reports whether requestedPath is a plan.md
|
||||
// variant (case-insensitive) sitting directly in the workspace home
|
||||
// directory.
|
||||
// The filename is compared case-insensitively because LLM output varies.
|
||||
func LooksLikeHomePlanFile(requestedPath, home string) bool {
|
||||
normalized := path.Clean(requestedPath)
|
||||
normalizedHome := path.Clean(home)
|
||||
|
||||
return looksLikePlanFileName(normalized) &&
|
||||
strings.EqualFold(path.Dir(normalized), normalizedHome)
|
||||
}
|
||||
|
||||
// looksLikeLegacySharedPlanPath reports whether requestedPath
|
||||
// matches the legacy shared plan path (case-insensitive). Used as a
|
||||
// narrow fallback when the workspace home cannot be resolved.
|
||||
func looksLikeLegacySharedPlanPath(requestedPath string) bool {
|
||||
normalized := path.Clean(requestedPath)
|
||||
return strings.EqualFold(normalized, LegacySharedPlanPath)
|
||||
}
|
||||
Reference in New Issue
Block a user