mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
feat(studio): a launch with no chosen or remembered project opens the app's own auto workspace, not home
This commit is contained in:
1 parent
7c22c5340e
commit
087ea05686
6 files changed
+142
-13
No files matched your search
@@ -6,19 +6,20 @@ import (
|
||||
"strings"
|
||||
|
||||
"reasonix/internal/assembly/boot"
|
||||
"reasonix/internal/contract/config"
|
||||
"reasonix/internal/frontend/serve"
|
||||
)
|
||||
|
||||
// launchWorkspace is the folder the first pane opens. The working directory
|
||||
// counts only when someone chose it: a Start-menu or Finder launch inherits the
|
||||
// launchWorkspace is the folder the first pane opens: a chosen directory, then
|
||||
// a remembered project, then the app's own auto workspace, never the home
|
||||
// directory. The working directory counts only when someone chose it: a Start-menu or Finder launch inherits the
|
||||
// install directory or the filesystem root, and taking that as a project puts
|
||||
// the app's own folder in the sidebar again after every removal.
|
||||
func launchWorkspace(appExe string) string {
|
||||
home, _ := os.UserHomeDir()
|
||||
return pickLaunchWorkspace(boot.ResolveWorkspaceRoot(""), appExe, serve.LaunchWorkspaces(), home)
|
||||
return pickLaunchWorkspace(boot.ResolveWorkspaceRoot(""), appExe, serve.LaunchWorkspaces(), config.AutoWorkspaceDir())
|
||||
}
|
||||
|
||||
func pickLaunchWorkspace(cwd, appExe string, remembered []string, home string) string {
|
||||
func pickLaunchWorkspace(cwd, appExe string, remembered []string, auto string) string {
|
||||
if userChoseDir(cwd, appExe) {
|
||||
return cwd
|
||||
}
|
||||
@@ -27,8 +28,8 @@ func pickLaunchWorkspace(cwd, appExe string, remembered []string, home string) s
|
||||
return dir
|
||||
}
|
||||
}
|
||||
if home != "" {
|
||||
return home
|
||||
if auto != "" && os.MkdirAll(auto, 0o755) == nil {
|
||||
return auto
|
||||
}
|
||||
return cwd
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/contract/config"
|
||||
)
|
||||
|
||||
func TestUserChoseDirRejectsWhatALauncherInherits(t *testing.T) {
|
||||
@@ -40,7 +43,8 @@ func TestPickLaunchWorkspaceFallbackOrder(t *testing.T) {
|
||||
}
|
||||
return d
|
||||
}
|
||||
a, b, home := mk("a"), mk("b"), mk("home")
|
||||
a, b := mk("a"), mk("b")
|
||||
auto := filepath.Join(base, "auto")
|
||||
gone := filepath.Join(base, "gone")
|
||||
proj := mk("proj")
|
||||
|
||||
@@ -53,11 +57,11 @@ func TestPickLaunchWorkspaceFallbackOrder(t *testing.T) {
|
||||
{"chosen directory wins over the list", proj, []string{a}, proj},
|
||||
{"list head", app, []string{a, b}, a},
|
||||
{"stale head skipped", app, []string{gone, b}, b},
|
||||
{"all stale falls to home", app, []string{gone}, home},
|
||||
{"empty list falls to home", app, nil, home},
|
||||
{"all stale falls to the auto workspace", app, []string{gone}, auto},
|
||||
{"empty list falls to the auto workspace", app, nil, auto},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := pickLaunchWorkspace(c.cwd, exe, c.remembered, home); got != c.want {
|
||||
if got := pickLaunchWorkspace(c.cwd, exe, c.remembered, auto); got != c.want {
|
||||
t.Errorf("%s: got %q, want %q", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
@@ -90,3 +94,54 @@ func TestLaunchWorkspaceKeepsLaunchProjectAfterSidebarReorder(t *testing.T) {
|
||||
t.Fatalf("missing launch project = %q, want next remembered folder %q", got, b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLaunchWithNothingChosenOrRememberedOpensTheAutoWorkspaceNotHome(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
state := filepath.Join(base, "state")
|
||||
home := filepath.Join(base, "home")
|
||||
app := filepath.Join(base, "app")
|
||||
for _, dir := range []string{state, home, app} {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
t.Setenv("REASONIX_HOME", state)
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
t.Chdir(app)
|
||||
exe := filepath.Join(app, "Reasonix Studio")
|
||||
|
||||
got := launchWorkspace(exe)
|
||||
if got == home {
|
||||
t.Fatalf("a first launch opened the home directory %q", got)
|
||||
}
|
||||
if want := config.AutoWorkspaceDir(); got != want {
|
||||
t.Fatalf("launchWorkspace = %q, want the auto workspace %q", got, want)
|
||||
}
|
||||
if st, err := os.Stat(got); err != nil || !st.IsDir() {
|
||||
t.Fatalf("the auto workspace was not created: %v", err)
|
||||
}
|
||||
if rel, err := filepath.Rel(state, got); err != nil || strings.HasPrefix(rel, "..") {
|
||||
t.Fatalf("the auto workspace %q is not under the state root %q", got, state)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickLaunchWorkspaceCreatesTheAutoWorkspaceLast(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
app := filepath.Join(base, "app")
|
||||
exe := filepath.Join(app, "Reasonix Studio")
|
||||
auto := filepath.Join(base, "state", "workspaces", "auto")
|
||||
if got := pickLaunchWorkspace(app, exe, nil, auto); got != auto {
|
||||
t.Fatalf("got %q, want %q", got, auto)
|
||||
}
|
||||
if st, err := os.Stat(auto); err != nil || !st.IsDir() {
|
||||
t.Fatalf("not created: %v", err)
|
||||
}
|
||||
chosen := filepath.Join(base, "proj")
|
||||
if err := os.MkdirAll(chosen, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := pickLaunchWorkspace(chosen, exe, nil, auto); got != chosen {
|
||||
t.Fatalf("a chosen directory must win over the auto workspace: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -555,6 +555,17 @@ func MemoryUserDir() string { return processRoots().MemoryUserDir() }
|
||||
// MemoryUserDir is the user state root this binding resolves to.
|
||||
func (r Roots) MemoryUserDir() string { return r.userSupportDir() }
|
||||
|
||||
// AutoWorkspaceDir is the folder a first launch opens when nobody has chosen a
|
||||
// project: a small one the app owns under the user state root, never the home
|
||||
// directory. The caller creates it.
|
||||
func AutoWorkspaceDir() string {
|
||||
base := processRoots().MemoryUserDir()
|
||||
if base == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(base, "workspaces", "auto")
|
||||
}
|
||||
|
||||
// ConventionDirs are the parent directories scanned for agent assets (skills,
|
||||
// commands), in canonical-first order. .reasonix is ours; .agents / .agent /
|
||||
// .claude let users drop in assets authored for other agent tools without moving
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/config"
|
||||
)
|
||||
|
||||
// The auto workspace is where an unchosen launch opens, not a project anyone
|
||||
// picked: if the list remembered it, it would become the answer to every later
|
||||
// launch and sit in the sidebar as a project.
|
||||
func TestTheAutoWorkspaceIsNeverRemembered(t *testing.T) {
|
||||
t.Setenv("REASONIX_HOME", testenv.TempDir(t))
|
||||
auto := config.AutoWorkspaceDir()
|
||||
if err := os.MkdirAll(auto, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
project := filepath.Join(testenv.TempDir(t), "project")
|
||||
if err := os.MkdirAll(project, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := addRememberedWorkspace(context.Background(), auto); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
keepUsedWorkspace(auto)
|
||||
if got := LaunchWorkspaces(); len(got) != 0 {
|
||||
t.Fatalf("the auto workspace was remembered: %v", got)
|
||||
}
|
||||
|
||||
if err := addRememberedWorkspace(context.Background(), project); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := LaunchWorkspaces(); len(got) != 1 || got[0] != project {
|
||||
t.Fatalf("a chosen project must still be remembered: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// What is said in an auto-workspace conversation is kept in the user state
|
||||
// root, with the conversations of every other folder: nothing about it lives in
|
||||
// a directory that can be deleted out from under it.
|
||||
func TestAutoWorkspaceConversationsAreKeptUnderTheStateRoot(t *testing.T) {
|
||||
state := testenv.TempDir(t)
|
||||
t.Setenv("REASONIX_HOME", state)
|
||||
dir := SessionDirFor(config.AutoWorkspaceDir())
|
||||
if rel, err := filepath.Rel(state, dir); err != nil || len(rel) == 0 || rel[0] == '.' {
|
||||
t.Fatalf("session dir %q is not under the state root %q", dir, state)
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,13 @@ func updateWorkspaceList(ctx context.Context, repair bool, mutate func(*workspac
|
||||
return workspacelist.Update(ctx, workspacesPath(), repair, mutate)
|
||||
}
|
||||
|
||||
// isAutoWorkspace says whether dir is the folder the app opens when nobody chose
|
||||
// one. It is where such a launch lands, not a project, so no list holds it.
|
||||
func isAutoWorkspace(dir string) bool {
|
||||
auto := config.AutoWorkspaceDir()
|
||||
return auto != "" && projectIdentity(dir) == projectIdentity(auto)
|
||||
}
|
||||
|
||||
func rememberWorkspace(dir string) {
|
||||
if err := addRememberedWorkspace(context.Background(), dir); err != nil {
|
||||
slog.Warn("serve: remember workspace", "err", err)
|
||||
@@ -65,7 +72,7 @@ func rememberWorkspace(dir string) {
|
||||
}
|
||||
|
||||
func addRememberedWorkspace(ctx context.Context, dir string) error {
|
||||
if dir == "" {
|
||||
if dir == "" || isAutoWorkspace(dir) {
|
||||
return nil
|
||||
}
|
||||
return updateWorkspaceList(ctx, true, func(list *workspaceList) error {
|
||||
@@ -92,7 +99,7 @@ func addRememberedWorkspace(ctx context.Context, dir string) error {
|
||||
// adopted at launch is no folder anyone picked, and its conversations are
|
||||
// reachable only through a row for it.
|
||||
func keepUsedWorkspace(dir string) {
|
||||
if dir == "" {
|
||||
if dir == "" || isAutoWorkspace(dir) {
|
||||
return
|
||||
}
|
||||
err := updateWorkspaceList(context.Background(), true, func(list *workspaceList) error {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
- 改进:没有选过项目也没有记住的项目时,首次启动不再打开用户主目录,而是打开应用自己管理的一个小目录;它不会被记成项目。关联 #14363。
|
||||
- Improved: with no chosen or remembered project, a first launch opens a small folder the app owns instead of your home directory, and that folder is never remembered as a project. Refs #14363.
|
||||
Reference in new issue
Block a user