Skip to content

Commit 2fc32fc

Browse files
committed
feat(agent-ui): add resizable split panes for chat and file view #453
Refactor AgentChatInterface to use ResizableSplitPane for flexible layout between chat, tree view, and file viewer. Improve FileViewerPanel with async loading, file size checks, and loading/error states.
1 parent e2f2da0 commit 2fc32fc

File tree

2 files changed

+210
-100
lines changed

2 files changed

+210
-100
lines changed

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

Lines changed: 107 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,113 @@ fun AgentChatInterface(
8383
return
8484
}
8585

86-
Row(modifier = modifier.fillMaxSize()) {
87-
// 左侧:Chat + Input 完整区域
88-
Column(
89-
modifier = Modifier
90-
.weight(1f)
91-
.fillMaxHeight()
92-
) {
86+
// 使用 ResizableSplitPane 分割 Chat 区域和 TreeView 区域
87+
if (viewModel.isTreeViewVisible) {
88+
ResizableSplitPane(
89+
modifier = modifier.fillMaxSize(),
90+
initialSplitRatio = 0.6f,
91+
minRatio = 0.3f,
92+
maxRatio = 0.8f,
93+
first = {
94+
// 左侧:Chat + Input 完整区域
95+
Column(modifier = Modifier.fillMaxSize()) {
96+
if (viewModel.isExecuting || viewModel.renderer.currentIteration > 0) {
97+
AgentStatusBar(
98+
isExecuting = viewModel.isExecuting,
99+
currentIteration = viewModel.renderer.currentIteration,
100+
maxIterations = viewModel.renderer.maxIterations,
101+
executionTime = viewModel.renderer.currentExecutionTime,
102+
viewModel = viewModel,
103+
onCancel = { viewModel.cancelTask() }
104+
)
105+
}
106+
107+
// Chat 消息列表
108+
AgentMessageList(
109+
renderer = viewModel.renderer,
110+
modifier = Modifier
111+
.fillMaxWidth()
112+
.weight(1f),
113+
onOpenFileViewer = { filePath ->
114+
viewModel.renderer.openFileViewer(filePath)
115+
}
116+
)
117+
118+
val callbacks = remember(viewModel) {
119+
createAgentCallbacks(
120+
viewModel = viewModel,
121+
onConfigWarning = onConfigWarning
122+
)
123+
}
124+
125+
// 输入框
126+
DevInEditorInput(
127+
initialText = "",
128+
placeholder = "Describe your coding task...",
129+
callbacks = callbacks,
130+
completionManager = currentWorkspace?.completionManager,
131+
isCompactMode = true,
132+
onModelConfigChange = { /* Handle model config change if needed */ },
133+
modifier =
134+
Modifier
135+
.fillMaxWidth()
136+
.imePadding()
137+
.padding(horizontal = 12.dp, vertical = 8.dp)
138+
)
139+
140+
ToolLoadingStatusBar(
141+
viewModel = viewModel,
142+
modifier = Modifier
143+
.fillMaxWidth()
144+
.padding(horizontal = 12.dp, vertical = 4.dp)
145+
)
146+
}
147+
},
148+
second = {
149+
// 右侧:TreeView + FileViewer(也使用 ResizableSplitPane)
150+
val hasFileViewer = viewModel.renderer.currentViewingFile != null
151+
if (hasFileViewer) {
152+
ResizableSplitPane(
153+
modifier = Modifier.fillMaxSize(),
154+
initialSplitRatio = 0.4f,
155+
minRatio = 0.2f,
156+
maxRatio = 0.6f,
157+
first = {
158+
FileSystemTreeView(
159+
rootPath = currentWorkspace?.rootPath ?: "",
160+
onFileClick = { filePath ->
161+
viewModel.renderer.openFileViewer(filePath)
162+
},
163+
onClose = { viewModel.closeTreeView() },
164+
modifier = Modifier.fillMaxSize()
165+
)
166+
},
167+
second = {
168+
viewModel.renderer.currentViewingFile?.let { filePath ->
169+
FileViewerPanelWrapper(
170+
filePath = filePath,
171+
onClose = { viewModel.renderer.closeFileViewer() },
172+
modifier = Modifier.fillMaxSize()
173+
)
174+
}
175+
}
176+
)
177+
} else {
178+
// 只有 TreeView
179+
FileSystemTreeView(
180+
rootPath = currentWorkspace?.rootPath ?: "",
181+
onFileClick = { filePath ->
182+
viewModel.renderer.openFileViewer(filePath)
183+
},
184+
onClose = { viewModel.closeTreeView() },
185+
modifier = Modifier.fillMaxSize()
186+
)
187+
}
188+
}
189+
)
190+
} else {
191+
// TreeView 未打开时的布局
192+
Column(modifier = modifier.fillMaxSize()) {
93193
if (viewModel.isExecuting || viewModel.renderer.currentIteration > 0) {
94194
AgentStatusBar(
95195
isExecuting = viewModel.isExecuting,
@@ -101,7 +201,6 @@ fun AgentChatInterface(
101201
)
102202
}
103203

104-
// Chat 消息列表
105204
AgentMessageList(
106205
renderer = viewModel.renderer,
107206
modifier = Modifier
@@ -119,7 +218,6 @@ fun AgentChatInterface(
119218
)
120219
}
121220

122-
// 输入框
123221
DevInEditorInput(
124222
initialText = "",
125223
placeholder = "Describe your coding task...",
@@ -141,38 +239,6 @@ fun AgentChatInterface(
141239
.padding(horizontal = 12.dp, vertical = 4.dp)
142240
)
143241
}
144-
145-
// 右侧:TreeView + FileViewer(左右分割)
146-
if (viewModel.isTreeViewVisible) {
147-
Row(
148-
modifier = Modifier
149-
.fillMaxHeight()
150-
) {
151-
// TreeView
152-
FileSystemTreeView(
153-
rootPath = currentWorkspace?.rootPath ?: "",
154-
onFileClick = { filePath ->
155-
viewModel.renderer.openFileViewer(filePath)
156-
},
157-
onClose = { viewModel.closeTreeView() },
158-
modifier = Modifier
159-
.width(280.dp)
160-
.fillMaxHeight()
161-
)
162-
163-
// FileViewer(可选)
164-
viewModel.renderer.currentViewingFile?.let { filePath ->
165-
VerticalDivider()
166-
FileViewerPanelWrapper(
167-
filePath = filePath,
168-
onClose = { viewModel.renderer.closeFileViewer() },
169-
modifier = Modifier
170-
.width(400.dp)
171-
.fillMaxHeight()
172-
)
173-
}
174-
}
175-
}
176242
}
177243

178244
}

mpp-ui/src/jvmMain/kotlin/cc/unitmesh/devins/ui/compose/agent/FileViewerPanel.jvm.kt

Lines changed: 103 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,41 @@ fun FileViewerPanel(
2323
var textArea by remember { mutableStateOf<RSyntaxTextArea?>(null) }
2424
var errorMessage by remember { mutableStateOf<String?>(null) }
2525
var fileName by remember { mutableStateOf("") }
26+
var isLoading by remember { mutableStateOf(true) }
27+
var fileContent by remember { mutableStateOf<String?>(null) }
2628

29+
// 异步加载文件内容
2730
LaunchedEffect(filePath) {
28-
try {
29-
val file = File(filePath)
30-
if (file.exists()) {
31+
isLoading = true
32+
errorMessage = null
33+
fileContent = null
34+
35+
kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
36+
try {
37+
val file = File(filePath)
38+
if (!file.exists()) {
39+
errorMessage = "File not found: $filePath"
40+
fileName = filePath
41+
return@withContext
42+
}
43+
3144
fileName = file.name
32-
errorMessage = null
33-
} else {
34-
errorMessage = "File not found: $filePath"
45+
46+
// 检查文件大小(限制 10MB)
47+
val maxSize = 10 * 1024 * 1024 // 10MB
48+
if (file.length() > maxSize) {
49+
errorMessage = "File too large (${file.length() / 1024 / 1024}MB). Maximum size is 10MB."
50+
return@withContext
51+
}
52+
53+
// 异步读取文件
54+
fileContent = file.readText()
55+
} catch (e: Exception) {
56+
errorMessage = "Error loading file: ${e.message}"
3557
fileName = filePath
58+
} finally {
59+
isLoading = false
3660
}
37-
} catch (e: Exception) {
38-
errorMessage = "Error: ${e.message}"
39-
fileName = filePath
4061
}
4162
}
4263

@@ -82,60 +103,83 @@ fun FileViewerPanel(
82103

83104
// Content
84105
Box(modifier = Modifier.fillMaxSize()) {
85-
if (errorMessage != null) {
86-
// Error state
87-
Column(
88-
modifier = Modifier
89-
.fillMaxSize()
90-
.padding(16.dp),
91-
horizontalAlignment = Alignment.CenterHorizontally,
92-
verticalArrangement = Arrangement.Center
93-
) {
94-
Text(
95-
text = "❌ Error",
96-
style = MaterialTheme.typography.titleMedium,
97-
color = MaterialTheme.colorScheme.error
98-
)
99-
Spacer(modifier = Modifier.height(8.dp))
100-
Text(
101-
text = errorMessage!!,
102-
style = MaterialTheme.typography.bodyMedium,
103-
color = MaterialTheme.colorScheme.onSurface
104-
)
106+
when {
107+
isLoading -> {
108+
// Loading state
109+
Column(
110+
modifier = Modifier
111+
.fillMaxSize()
112+
.padding(16.dp),
113+
horizontalAlignment = Alignment.CenterHorizontally,
114+
verticalArrangement = Arrangement.Center
115+
) {
116+
CircularProgressIndicator(
117+
modifier = Modifier.size(48.dp)
118+
)
119+
Spacer(modifier = Modifier.height(16.dp))
120+
Text(
121+
text = "Loading file...",
122+
style = MaterialTheme.typography.bodyMedium,
123+
color = MaterialTheme.colorScheme.onSurface
124+
)
125+
}
105126
}
106-
} else {
107-
// RSyntaxTextArea content
108-
SwingPanel(
109-
background = MaterialTheme.colorScheme.surface,
110-
modifier = Modifier.fillMaxSize(),
111-
factory = {
112-
val file = File(filePath)
113-
val area = RSyntaxTextArea().apply {
114-
text = if (file.exists()) file.readText() else ""
115-
isEditable = false
116-
syntaxEditingStyle = getSyntaxStyleForFile(file)
117-
isCodeFoldingEnabled = true
118-
antiAliasingEnabled = true
119-
tabSize = 4
120-
margin = java.awt.Insets(5, 5, 5, 5)
121-
}
122-
textArea = area
123-
124-
RTextScrollPane(area).apply {
125-
isFoldIndicatorEnabled = true
126-
}
127-
},
128-
update = {
129-
// Update text if file path changes
130-
textArea?.let { area ->
127+
errorMessage != null -> {
128+
// Error state
129+
Column(
130+
modifier = Modifier
131+
.fillMaxSize()
132+
.padding(16.dp),
133+
horizontalAlignment = Alignment.CenterHorizontally,
134+
verticalArrangement = Arrangement.Center
135+
) {
136+
Text(
137+
text = "❌ Error",
138+
style = MaterialTheme.typography.titleMedium,
139+
color = MaterialTheme.colorScheme.error
140+
)
141+
Spacer(modifier = Modifier.height(8.dp))
142+
Text(
143+
text = errorMessage!!,
144+
style = MaterialTheme.typography.bodyMedium,
145+
color = MaterialTheme.colorScheme.onSurface
146+
)
147+
}
148+
}
149+
fileContent != null -> {
150+
// RSyntaxTextArea content
151+
SwingPanel(
152+
background = MaterialTheme.colorScheme.surface,
153+
modifier = Modifier.fillMaxSize(),
154+
factory = {
131155
val file = File(filePath)
132-
if (file.exists()) {
133-
area.text = file.readText()
134-
area.syntaxEditingStyle = getSyntaxStyleForFile(file)
156+
val area = RSyntaxTextArea().apply {
157+
text = fileContent ?: ""
158+
isEditable = false
159+
syntaxEditingStyle = getSyntaxStyleForFile(file)
160+
isCodeFoldingEnabled = true
161+
antiAliasingEnabled = true
162+
tabSize = 4
163+
margin = java.awt.Insets(5, 5, 5, 5)
164+
}
165+
textArea = area
166+
167+
RTextScrollPane(area).apply {
168+
isFoldIndicatorEnabled = true
169+
}
170+
},
171+
update = {
172+
// Update text if content changes
173+
textArea?.let { area ->
174+
if (area.text != fileContent) {
175+
val file = File(filePath)
176+
area.text = fileContent ?: ""
177+
area.syntaxEditingStyle = getSyntaxStyleForFile(file)
178+
}
135179
}
136180
}
137-
}
138-
)
181+
)
182+
}
139183
}
140184
}
141185
}

0 commit comments

Comments
 (0)