Skip to content

Commit 25f606f

Browse files
committed
feat(ui): add persistent last workspace and cross-platform keymap
#453 Implement persistent storage and auto-loading of the last opened workspace. Introduce a cross-platform keyboard shortcut keymap for desktop UI, and update menu shortcuts to use it.
1 parent 32d228b commit 25f606f

File tree

8 files changed

+228
-40
lines changed

8 files changed

+228
-40
lines changed

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/AutoDevApp.kt

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -137,38 +137,60 @@ private fun AutoDevContent(
137137

138138
LaunchedEffect(Unit) {
139139
if (!WorkspaceManager.hasActiveWorkspace()) {
140-
val defaultPath = when {
141-
Platform.isAndroid -> "/storage/emulated/0/Documents"
142-
Platform.isJs -> "."
143-
else -> "${Platform.getUserHomeDir()}/AutoDevProjects"
140+
// Try to load last workspace first
141+
val lastWorkspace = try {
142+
ConfigManager.getLastWorkspace()
143+
} catch (e: Exception) {
144+
println("⚠️ 加载上次工作空间失败: ${e.message}")
145+
null
146+
}
147+
148+
if (lastWorkspace != null) {
149+
val fileSystem = DefaultFileSystem(lastWorkspace.path)
150+
if (fileSystem.exists(lastWorkspace.path)) {
151+
println("✅ 加载上次工作空间: ${lastWorkspace.name} (${lastWorkspace.path})")
152+
WorkspaceManager.openWorkspace(lastWorkspace.name, lastWorkspace.path)
153+
} else {
154+
println("⚠️ 上次工作空间不存在: ${lastWorkspace.path}")
155+
// Fall through to default workspace logic
156+
}
144157
}
158+
159+
// If last workspace not available or doesn't exist, use default
160+
if (!WorkspaceManager.hasActiveWorkspace()) {
161+
val defaultPath = when {
162+
Platform.isAndroid -> "/storage/emulated/0/Documents"
163+
Platform.isJs -> "."
164+
else -> "${Platform.getUserHomeDir()}/AutoDevProjects"
165+
}
145166

146-
val fileSystem = DefaultFileSystem(defaultPath)
167+
val fileSystem = DefaultFileSystem(defaultPath)
147168

148-
if (fileSystem.exists(defaultPath)) {
149-
WorkspaceManager.openWorkspace("Default Workspace", defaultPath)
150-
} else {
151-
when {
152-
Platform.isAndroid -> {
153-
val fallbackPath = "/sdcard"
154-
println("⚠️ Documents 目录不存在,使用备用路径: $fallbackPath")
155-
WorkspaceManager.openWorkspace("Default Workspace", fallbackPath)
156-
}
169+
if (fileSystem.exists(defaultPath)) {
170+
WorkspaceManager.openWorkspace("Default Workspace", defaultPath)
171+
} else {
172+
when {
173+
Platform.isAndroid -> {
174+
val fallbackPath = "/sdcard"
175+
println("⚠️ Documents 目录不存在,使用备用路径: $fallbackPath")
176+
WorkspaceManager.openWorkspace("Default Workspace", fallbackPath)
177+
}
157178

158-
Platform.isJs -> {
159-
println("⚠️ 使用当前工作目录")
160-
WorkspaceManager.openWorkspace("Current Directory", ".")
161-
}
179+
Platform.isJs -> {
180+
println("⚠️ 使用当前工作目录")
181+
WorkspaceManager.openWorkspace("Current Directory", ".")
182+
}
162183

163-
else -> {
164-
try {
165-
fileSystem.createDirectory(defaultPath)
166-
println("✅ 创建默认工作空间目录: $defaultPath")
167-
WorkspaceManager.openWorkspace("Default Workspace", defaultPath)
168-
} catch (e: Exception) {
169-
println("⚠️ 无法创建默认目录,使用用户主目录")
170-
val homeDir = Platform.getUserHomeDir()
171-
WorkspaceManager.openWorkspace("Home Directory", homeDir)
184+
else -> {
185+
try {
186+
fileSystem.createDirectory(defaultPath)
187+
println("✅ 创建默认工作空间目录: $defaultPath")
188+
WorkspaceManager.openWorkspace("Default Workspace", defaultPath)
189+
} catch (e: Exception) {
190+
println("⚠️ 无法创建默认目录,使用用户主目录")
191+
val homeDir = Platform.getUserHomeDir()
192+
WorkspaceManager.openWorkspace("Home Directory", homeDir)
193+
}
172194
}
173195
}
174196
}
@@ -249,6 +271,14 @@ private fun AutoDevContent(
249271
try {
250272
WorkspaceManager.openWorkspace(projectName, path)
251273
println("📁 已切换项目路径: $path")
274+
275+
// Save the last workspace to config
276+
try {
277+
ConfigManager.saveLastWorkspace(projectName, path)
278+
println("✅ 已保存工作空间到配置")
279+
} catch (e: Exception) {
280+
println("⚠️ 保存工作空间配置失败: ${e.message}")
281+
}
252282
} catch (e: Exception) {
253283
errorMessage = "切换工作空间失败: ${e.message}"
254284
showErrorDialog = true

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/AgentInterfaceRouter.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package cc.unitmesh.devins.ui.compose.agent
22

33
import androidx.compose.runtime.Composable
44
import androidx.compose.ui.Modifier
5-
import cc.unitmesh.agent.CodeReviewAgent
65
import cc.unitmesh.devins.ui.compose.agent.codereview.CodeReviewPage
76
import cc.unitmesh.llm.KoogLLMService
87

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/codereview/CodeReviewViewModel.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,6 @@ open class CodeReviewViewModel(
228228
AutoDevLogger.info("CodeReviewViewModel") {
229229
"🤖 Auto-starting analysis with ${diffFiles.size} files"
230230
}
231-
startAnalysis()
232231
}
233232

234233
} catch (e: Exception) {

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/config/ConfigFile.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public data class ConfigFile(
3737
val mcpServers: Map<String, McpServerConfig>? = emptyMap(),
3838
val language: String? = "en", // Language preference: "en" or "zh"
3939
val remoteServer: RemoteServerConfig? = null,
40-
val agentType: String? = "Local" // "Local" or "Remote" - which agent mode to use
40+
val agentType: String? = "Local", // "Local" or "Remote" - which agent mode to use
41+
val lastWorkspace: WorkspaceInfo? = null // Last opened workspace information
4142
)
4243

4344
/**
@@ -50,6 +51,15 @@ data class RemoteServerConfig(
5051
val useServerConfig: Boolean = false // Whether to use server's LLM config instead of local
5152
)
5253

54+
/**
55+
* Last opened workspace information
56+
*/
57+
@Serializable
58+
data class WorkspaceInfo(
59+
val name: String,
60+
val path: String
61+
)
62+
5363
class AutoDevConfigWrapper(val configFile: ConfigFile) {
5464
fun getActiveConfig(): NamedModelConfig? {
5565
if (configFile.active.isEmpty() || configFile.configs.isEmpty()) {
@@ -99,4 +109,8 @@ class AutoDevConfigWrapper(val configFile: ConfigFile) {
99109
fun getAgentType(): String {
100110
return configFile.agentType ?: "Local"
101111
}
112+
113+
fun getLastWorkspace(): WorkspaceInfo? {
114+
return configFile.lastWorkspace
115+
}
102116
}

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/config/ConfigManager.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,19 @@ expect object ConfigManager {
105105
* @return A unique name (either baseName or baseName-1, baseName-2, etc.)
106106
*/
107107
fun generateUniqueConfigName(baseName: String, existingNames: List<String>): String
108+
109+
/**
110+
* Save last opened workspace
111+
*
112+
* @param name Workspace name
113+
* @param path Workspace absolute path
114+
*/
115+
suspend fun saveLastWorkspace(name: String, path: String)
116+
117+
/**
118+
* Get last opened workspace
119+
*
120+
* @return WorkspaceInfo or null if no workspace was saved
121+
*/
122+
suspend fun getLastWorkspace(): WorkspaceInfo?
108123
}

mpp-ui/src/jvmMain/kotlin/cc/unitmesh/devins/ui/config/ConfigManager.jvm.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,4 +211,19 @@ actual object ConfigManager {
211211
private fun toYaml(configFile: ConfigFile): String {
212212
return YamlUtils.dump(configFile, kotlinx.serialization.serializer())
213213
}
214+
215+
actual suspend fun saveLastWorkspace(name: String, path: String) {
216+
val wrapper = load()
217+
val configFile = wrapper.configFile
218+
219+
val updatedConfigFile = configFile.copy(
220+
lastWorkspace = WorkspaceInfo(name = name, path = path)
221+
)
222+
save(updatedConfigFile)
223+
}
224+
225+
actual suspend fun getLastWorkspace(): WorkspaceInfo? {
226+
val wrapper = load()
227+
return wrapper.getLastWorkspace()
228+
}
214229
}

mpp-ui/src/jvmMain/kotlin/cc/unitmesh/devins/ui/desktop/AutoDevMenuBar.kt

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package cc.unitmesh.devins.ui.desktop
33
import androidx.compose.runtime.Composable
44
import androidx.compose.runtime.rememberCoroutineScope
55
import androidx.compose.ui.ExperimentalComposeUiApi
6-
import androidx.compose.ui.input.key.Key
7-
import androidx.compose.ui.input.key.KeyShortcut
86
import androidx.compose.ui.window.FrameWindowScope
97
import androidx.compose.ui.window.MenuBar
108
import cc.unitmesh.devins.ui.compose.theme.ThemeManager
@@ -16,7 +14,7 @@ import kotlinx.coroutines.launch
1614
* AutoDev Desktop 菜单栏
1715
*
1816
* 提供常用的桌面应用菜单功能:
19-
* - File 菜单:打开项目 (Ctrl+O)、退出
17+
* - File 菜单:打开项目 (Cmd+O on Mac / Ctrl+O on others)、退出
2018
* - Edit 菜单:复制、粘贴等(未来扩展)
2119
* - View 菜单:语言切换、主题切换
2220
* - Help 菜单:关于、文档等(未来扩展)
@@ -35,7 +33,7 @@ fun FrameWindowScope.AutoDevMenuBar(
3533
Item(
3634
"Open Project...",
3735
onClick = onOpenFile,
38-
shortcut = KeyShortcut(Key.O, ctrl = true),
36+
shortcut = Keymap.openProject,
3937
mnemonic = 'O'
4038
)
4139

@@ -44,7 +42,7 @@ fun FrameWindowScope.AutoDevMenuBar(
4442
Item(
4543
"Exit",
4644
onClick = onExit,
47-
shortcut = KeyShortcut(Key.Q, ctrl = true),
45+
shortcut = Keymap.exitApp,
4846
mnemonic = 'x'
4947
)
5048
}
@@ -54,15 +52,15 @@ fun FrameWindowScope.AutoDevMenuBar(
5452
Item(
5553
"Copy",
5654
onClick = { /* TODO: 实现复制功能 */ },
57-
shortcut = KeyShortcut(Key.C, ctrl = true),
55+
shortcut = Keymap.copy,
5856
mnemonic = 'C',
5957
enabled = false
6058
)
6159

6260
Item(
6361
"Paste",
6462
onClick = { /* TODO: 实现粘贴功能 */ },
65-
shortcut = KeyShortcut(Key.V, ctrl = true),
63+
shortcut = Keymap.paste,
6664
mnemonic = 'P',
6765
enabled = false
6866
)
@@ -98,23 +96,23 @@ fun FrameWindowScope.AutoDevMenuBar(
9896
// Theme submenu
9997
Menu("Theme") {
10098
Item(
101-
"\uFE0F Light",
99+
"Light",
102100
onClick = {
103101
ThemeManager.setTheme(ThemeManager.ThemeMode.LIGHT)
104102
},
105103
mnemonic = 'L'
106104
)
107105

108106
Item(
109-
"\uD83C\uDF19 Dark",
107+
"Dark",
110108
onClick = {
111109
ThemeManager.setTheme(ThemeManager.ThemeMode.DARK)
112110
},
113111
mnemonic = 'D'
114112
)
115113

116114
Item(
117-
"\uD83D\uDD06 Auto (System)",
115+
"Auto (System)",
118116
onClick = {
119117
ThemeManager.setTheme(ThemeManager.ThemeMode.SYSTEM)
120118
},
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package cc.unitmesh.devins.ui.desktop
2+
3+
import androidx.compose.ui.ExperimentalComposeUiApi
4+
import androidx.compose.ui.input.key.Key
5+
import androidx.compose.ui.input.key.KeyShortcut
6+
import cc.unitmesh.agent.Platform
7+
8+
/**
9+
* Cross-platform keyboard shortcut definitions
10+
*
11+
* On macOS, uses Command (Meta) key for standard shortcuts
12+
* On Windows/Linux, uses Ctrl key
13+
*
14+
* Example:
15+
* ```kotlin
16+
* Item(
17+
* "Open Project...",
18+
* onClick = onOpenFile,
19+
* shortcut = Keymap.openProject,
20+
* mnemonic = 'O'
21+
* )
22+
* ```
23+
*/
24+
@OptIn(ExperimentalComposeUiApi::class)
25+
object Keymap {
26+
private val isMac: Boolean = Platform.getOSName().contains("mac", ignoreCase = true)
27+
28+
/**
29+
* Open Project: Command+O (Mac) / Ctrl+O (Windows/Linux)
30+
*/
31+
val openProject: KeyShortcut = KeyShortcut(
32+
Key.O,
33+
meta = isMac,
34+
ctrl = !isMac
35+
)
36+
37+
/**
38+
* Exit Application: Command+Q (Mac) / Ctrl+Q (Windows/Linux)
39+
*/
40+
val exitApp: KeyShortcut = KeyShortcut(
41+
Key.Q,
42+
meta = isMac,
43+
ctrl = !isMac
44+
)
45+
46+
/**
47+
* Copy: Command+C (Mac) / Ctrl+C (Windows/Linux)
48+
*/
49+
val copy: KeyShortcut = KeyShortcut(
50+
Key.C,
51+
meta = isMac,
52+
ctrl = !isMac
53+
)
54+
55+
/**
56+
* Paste: Command+V (Mac) / Ctrl+V (Windows/Linux)
57+
*/
58+
val paste: KeyShortcut = KeyShortcut(
59+
Key.V,
60+
meta = isMac,
61+
ctrl = !isMac
62+
)
63+
64+
/**
65+
* Save: Command+S (Mac) / Ctrl+S (Windows/Linux)
66+
*/
67+
val save: KeyShortcut = KeyShortcut(
68+
Key.S,
69+
meta = isMac,
70+
ctrl = !isMac
71+
)
72+
73+
/**
74+
* Undo: Command+Z (Mac) / Ctrl+Z (Windows/Linux)
75+
*/
76+
val undo: KeyShortcut = KeyShortcut(
77+
Key.Z,
78+
meta = isMac,
79+
ctrl = !isMac
80+
)
81+
82+
/**
83+
* Redo: Command+Shift+Z (Mac) / Ctrl+Shift+Z (Windows/Linux)
84+
*/
85+
val redo: KeyShortcut = KeyShortcut(
86+
Key.Z,
87+
meta = isMac,
88+
ctrl = !isMac,
89+
shift = true
90+
)
91+
92+
/**
93+
* New: Command+N (Mac) / Ctrl+N (Windows/Linux)
94+
*/
95+
val new: KeyShortcut = KeyShortcut(
96+
Key.N,
97+
meta = isMac,
98+
ctrl = !isMac
99+
)
100+
101+
/**
102+
* Find: Command+F (Mac) / Ctrl+F (Windows/Linux)
103+
*/
104+
val find: KeyShortcut = KeyShortcut(
105+
Key.F,
106+
meta = isMac,
107+
ctrl = !isMac
108+
)
109+
110+
/**
111+
* Select All: Command+A (Mac) / Ctrl+A (Windows/Linux)
112+
*/
113+
val selectAll: KeyShortcut = KeyShortcut(
114+
Key.A,
115+
meta = isMac,
116+
ctrl = !isMac
117+
)
118+
}

0 commit comments

Comments
 (0)