返回 DeepSeek-Reasonix
workspace.go
根目录 / desktop / workspace.go
1 package main
2
3 import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "strings"
8
9 "reasonix/internal/config"
10 )
11
12 // The desktop is a GUI app: launched from Finder or `open`, it starts with the
13 // working directory set to "/" (read-only), so anything cwd-relative — config,
14 // .env writes, memory/skill discovery — fails or lands nowhere useful. We keep a
15 // real working folder instead: remember the last one the user picked and chdir
16 // into it at startup, falling back to the home directory when there's none and
17 // cwd isn't writable.
18
19 // workspaceStatePath is where the last working folder is remembered (under the
20 // user config dir, shared with the rest of Reasonix's state).
21 func workspaceStatePath() string {
22 dir := config.MemoryUserDir() // …/reasonix
23 if dir == "" {
24 return ""
25 }
26 return filepath.Join(dir, "desktop-workspace")
27 }
28
29 func workspaceListPath() string {
30 dir := config.MemoryUserDir()
31 if dir == "" {
32 return ""
33 }
34 return filepath.Join(dir, "desktop-workspaces.json")
35 }
36
37 // saveWorkspace records dir as the last working folder.
38 func saveWorkspace(dir string) {
39 p := workspaceStatePath()
40 if p == "" || dir == "" {
41 return
42 }
43 if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
44 return
45 }
46 _ = os.WriteFile(p, []byte(dir), 0o644)
47 }
48
49 // clearWorkspace removes the active workspace pointer file so that no stale
50 // reference remains after the current workspace is deleted.
51 func clearWorkspace() {
52 p := workspaceStatePath()
53 if p == "" {
54 return
55 }
56 _ = os.Remove(p)
57 }
58
59 // loadWorkspace returns the remembered working folder, or "" if none.
60 func loadWorkspace() string {
61 p := workspaceStatePath()
62 if p == "" {
63 return ""
64 }
65 b, err := readFileUTF8(p)
66 if err != nil {
67 return ""
68 }
69 return strings.TrimSpace(string(b))
70 }
71
72 func loadWorkspaces() []string {
73 p := workspaceListPath()
74 if p == "" {
75 return nil
76 }
77 var paths []string
78 b, err := readFileUTF8(p)
79 if err != nil || json.Unmarshal(b, &paths) != nil {
80 return nil
81 }
82 out := make([]string, 0, len(paths))
83 seen := map[string]bool{}
84 for _, path := range paths {
85 path = strings.TrimSpace(path)
86 if path == "" || seen[path] {
87 continue
88 }
89 seen[path] = true
90 out = append(out, path)
91 }
92 return out
93 }
94
95 func rememberWorkspace(dir string) {
96 dir = strings.TrimSpace(dir)
97 if dir == "" {
98 return
99 }
100 if abs, err := filepath.Abs(dir); err == nil {
101 dir = abs
102 }
103 paths := []string{dir}
104 for _, path := range loadWorkspaces() {
105 if path != dir {
106 paths = append(paths, path)
107 }
108 if len(paths) >= 12 {
109 break
110 }
111 }
112 p := workspaceListPath()
113 if p == "" {
114 return
115 }
116 if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
117 return
118 }
119 if b, err := json.MarshalIndent(paths, "", " "); err == nil {
120 _ = os.WriteFile(p, b, 0o644)
121 }
122 }
123
124 func forgetWorkspace(dir string) {
125 dir = strings.TrimSpace(dir)
126 if dir == "" {
127 return
128 }
129 if abs, err := filepath.Abs(dir); err == nil {
130 dir = abs
131 }
132 paths := make([]string, 0, len(loadWorkspaces()))
133 for _, path := range loadWorkspaces() {
134 if abs, err := filepath.Abs(path); err == nil {
135 path = abs
136 }
137 if path != dir {
138 paths = append(paths, path)
139 }
140 }
141 p := workspaceListPath()
142 if p == "" {
143 return
144 }
145 if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
146 return
147 }
148 if b, err := json.MarshalIndent(paths, "", " "); err == nil {
149 _ = os.WriteFile(p, b, 0o644)
150 }
151 }
152
153 // ensureWorkspace establishes a writable working directory at startup: the
154 // remembered folder if it's still a directory, else the home directory when the
155 // current cwd isn't writable (the Finder/`open` "/" case). A writable cwd with no
156 // remembered folder (e.g. `wails dev` in the repo) is left untouched.
157 func ensureWorkspace() {
158 if ws := loadWorkspace(); ws != "" {
159 if info, err := os.Stat(ws); err == nil && info.IsDir() && os.Chdir(ws) == nil {
160 return
161 }
162 }
163 if cwdWritable() {
164 return
165 }
166 if home, err := os.UserHomeDir(); err == nil {
167 _ = os.Chdir(home)
168 }
169 }
170
171 // cwdWritable reports whether the current directory accepts a file write — the
172 // reliable test for the read-only "/" a GUI launch lands in.
173 func cwdWritable() bool {
174 cwd, err := os.Getwd()
175 if err != nil {
176 return false
177 }
178 f, err := os.CreateTemp(cwd, ".reasonix-wtest-*")
179 if err != nil {
180 return false
181 }
182 name := f.Name()
183 _ = f.Close()
184 _ = os.Remove(name)
185 return true
186 }
187
187 lines GO