Skip to content

Commit 00aec64

Browse files
committed
Refactor panel components to use PickerList for improved rendering and functionality
- Replaced manual rendering logic in ProfilePanel, RunningAgentsPanel, SkillsPickerPanel, and TodoPickerPanel with PickerList component. - Simplified state management and rendering logic for displaying items, including scroll hints and selected states. - Enhanced user experience by providing consistent item rendering and improved handling of empty states and loading indicators.
1 parent 0f7a5ae commit 00aec64

16 files changed

Lines changed: 1121 additions & 982 deletions

JetBrains/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ plugins {
55
}
66

77
group = "com.snow"
8-
version = "0.4.7"
8+
version = "0.4.18"
99

1010
repositories {
1111
mavenCentral()

JetBrains/gradlew.bat

Lines changed: 85 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,31 @@
11
package com.snow.plugin
22

3+
import com.intellij.openapi.actionSystem.ActionManager
4+
import com.intellij.openapi.actionSystem.DefaultActionGroup
5+
import com.intellij.openapi.application.ApplicationManager
36
import com.intellij.openapi.project.Project
47
import com.intellij.openapi.startup.ProjectActivity
58

69
class SnowProjectActivity : ProjectActivity {
710
override suspend fun execute(project: Project) {
811
SnowPluginLifecycle.setupProject(project)
12+
ApplicationManager.getApplication().invokeLater {
13+
registerProjectViewAction()
14+
}
15+
}
16+
17+
companion object {
18+
@Volatile
19+
private var registered = false
20+
21+
private fun registerProjectViewAction() {
22+
if (registered) return
23+
val actionManager = ActionManager.getInstance()
24+
val sendAction = actionManager.getAction("snow.SendToSnowCLI") ?: return
25+
val group = actionManager.getAction("ProjectViewPopupMenu") as? DefaultActionGroup ?: return
26+
group.addSeparator()
27+
group.add(sendAction)
28+
registered = true
29+
}
930
}
1031
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.snow.plugin.actions
2+
3+
import com.intellij.openapi.actionSystem.ActionUpdateThread
4+
import com.intellij.openapi.actionSystem.AnActionEvent
5+
import com.intellij.openapi.actionSystem.CommonDataKeys
6+
import com.intellij.openapi.application.ApplicationManager
7+
import com.intellij.openapi.project.DumbAwareAction
8+
import com.snow.plugin.SnowWebSocketManager
9+
import com.snow.plugin.util.TerminalCompat
10+
11+
class SendToSnowCLIAction : DumbAwareAction() {
12+
13+
override fun getActionUpdateThread() = ActionUpdateThread.BGT
14+
15+
override fun actionPerformed(e: AnActionEvent) {
16+
val project = e.project ?: return
17+
val files = e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY)
18+
?: e.getData(CommonDataKeys.VIRTUAL_FILE)?.let { arrayOf(it) }
19+
?: return
20+
if (files.isEmpty()) return
21+
22+
val formattedPaths = files.joinToString(" ") { "\"${it.path}\"" }
23+
24+
ApplicationManager.getApplication().invokeLater {
25+
val sent = TerminalCompat.sendTextToNamedTerminal(project, "Snow CLI", formattedPaths)
26+
if (!sent) {
27+
TerminalCompat.openTerminalWithCommand(project, project.basePath, "Snow CLI", "snow")
28+
ApplicationManager.getApplication().executeOnPooledThread {
29+
Thread.sleep(3000)
30+
ApplicationManager.getApplication().invokeLater {
31+
TerminalCompat.sendTextToNamedTerminal(project, "Snow CLI", formattedPaths)
32+
}
33+
}
34+
val wsManager = SnowWebSocketManager.instance
35+
ApplicationManager.getApplication().executeOnPooledThread {
36+
Thread.sleep(500)
37+
wsManager.connect()
38+
}
39+
}
40+
}
41+
}
42+
43+
override fun update(e: AnActionEvent) {
44+
e.presentation.isVisible = true
45+
e.presentation.isEnabled = e.project != null
46+
}
47+
}

JetBrains/src/main/kotlin/com/snow/plugin/util/TerminalCompat.kt

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,108 @@ package com.snow.plugin.util
22

33
import com.intellij.openapi.application.ApplicationManager
44
import com.intellij.openapi.project.Project
5+
import com.intellij.openapi.wm.ToolWindowManager
56

67
/**
78
* Compatibility layer for terminal API across IntelliJ versions.
89
* Uses Reworked Terminal API (2025.3+) when available, falls back to classic API via reflection.
910
*/
1011
object TerminalCompat {
1112

13+
@Volatile
14+
private var lastTerminalRef: Any? = null
15+
1216
fun openTerminalWithCommand(project: Project, workingDirectory: String?, tabName: String, command: String) {
1317
if (!tryReworkedApi(project, workingDirectory, tabName, command)) {
1418
fallbackClassicApi(project, workingDirectory, tabName, command)
1519
}
1620
}
1721

22+
/**
23+
* Send text to an existing Snow CLI terminal (without pressing Enter).
24+
* Uses saved terminal reference first, falls back to component tree search.
25+
*/
26+
fun sendTextToNamedTerminal(project: Project, tabName: String, text: String): Boolean {
27+
// Strategy 1: use the saved reference from openTerminalWithCommand
28+
lastTerminalRef?.let { ref ->
29+
if (trySendTextViaRef(ref, text)) {
30+
activateTerminalTab(project, tabName)
31+
return true
32+
}
33+
}
34+
35+
// Strategy 2: search component tree in the matching terminal tab
36+
val toolWindow = ToolWindowManager.getInstance(project).getToolWindow("Terminal")
37+
?: return false
38+
val content = toolWindow.contentManager.contents.firstOrNull {
39+
it.displayName == tabName || it.displayName.contains("Snow", ignoreCase = true)
40+
} ?: return false
41+
42+
toolWindow.contentManager.setSelectedContent(content)
43+
toolWindow.activate(null, false, false)
44+
return sendTextToComponentTree(content.component, text)
45+
}
46+
47+
private fun trySendTextViaRef(ref: Any, text: String): Boolean {
48+
// Reworked API: TerminalView.sendText(String)
49+
try {
50+
ref.javaClass.getMethod("sendText", String::class.java).invoke(ref, text)
51+
return true
52+
} catch (_: Exception) {}
53+
54+
// Classic API: widget.getTtyConnector().write(String)
55+
try {
56+
val connector = ref.javaClass.getMethod("getTtyConnector").invoke(ref)
57+
if (connector != null) {
58+
connector.javaClass.getMethod("write", String::class.java).invoke(connector, text)
59+
return true
60+
}
61+
} catch (_: Exception) {}
62+
63+
return false
64+
}
65+
66+
private fun activateTerminalTab(project: Project, tabName: String) {
67+
val toolWindow = ToolWindowManager.getInstance(project).getToolWindow("Terminal") ?: return
68+
val content = toolWindow.contentManager.contents.firstOrNull {
69+
it.displayName == tabName || it.displayName.contains("Snow", ignoreCase = true)
70+
}
71+
if (content != null) {
72+
toolWindow.contentManager.setSelectedContent(content)
73+
}
74+
toolWindow.activate(null, false, false)
75+
}
76+
77+
private fun sendTextToComponentTree(root: java.awt.Component, text: String): Boolean {
78+
if (trySendTextViaComponent(root, text)) return true
79+
if (root is java.awt.Container) {
80+
for (i in 0 until root.componentCount) {
81+
if (sendTextToComponentTree(root.getComponent(i), text)) return true
82+
}
83+
}
84+
return false
85+
}
86+
87+
private fun trySendTextViaComponent(component: Any, text: String): Boolean {
88+
val className = component.javaClass.name
89+
if (className.startsWith("javax.swing.") || className.startsWith("java.awt.")) return false
90+
91+
try {
92+
val connector = component.javaClass.getMethod("getTtyConnector").invoke(component)
93+
if (connector != null) {
94+
connector.javaClass.getMethod("write", String::class.java).invoke(connector, text)
95+
return true
96+
}
97+
} catch (_: Exception) {}
98+
99+
try {
100+
component.javaClass.getMethod("sendText", String::class.java).invoke(component, text)
101+
return true
102+
} catch (_: Exception) {}
103+
104+
return false
105+
}
106+
18107
private fun tryReworkedApi(
19108
project: Project, workingDirectory: String?, tabName: String, command: String
20109
): Boolean {
@@ -38,6 +127,8 @@ object TerminalCompat {
38127
val view = tClass.getMethod("getView").invoke(tab)!!
39128
val vClass = Class.forName("com.intellij.terminal.frontend.view.TerminalView")
40129

130+
lastTerminalRef = view
131+
41132
scheduleCommand {
42133
vClass.getMethod("sendText", String::class.java).invoke(view, "$command\n")
43134
}
@@ -59,6 +150,8 @@ object TerminalCompat {
59150
java.lang.Boolean.TYPE, java.lang.Boolean.TYPE
60151
).invoke(mgr, workingDirectory, tabName, true, true)!!
61152

153+
lastTerminalRef = widget
154+
62155
scheduleCommand {
63156
widget.javaClass.getMethod("sendCommandToExecute", String::class.java)
64157
.invoke(widget, command)

JetBrains/src/main/resources/META-INF/plugin.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@
5555
<add-to-group group-id="ToolsMenu" anchor="last"/>
5656
<keyboard-shortcut first-keystroke="control alt S" keymap="$default"/>
5757
</action>
58+
59+
<action id="snow.SendToSnowCLI"
60+
class="com.snow.plugin.actions.SendToSnowCLIAction"
61+
text="Send to Snow CLI"
62+
description="Send file path to Snow CLI terminal input">
63+
<add-to-group group-id="EditorPopupMenu" anchor="last"/>
64+
<add-to-group group-id="EditorTabPopupMenu" anchor="last"/>
65+
</action>
5866
</actions>
5967

6068
<applicationListeners>

source/agents/reviewAgent.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,15 @@ export class ReviewAgent {
138138
execSync('git diff --cached --quiet', {
139139
cwd: gitRoot,
140140
encoding: 'utf-8',
141+
stdio: 'pipe',
141142
});
142143
} catch {
143144
hasStaged = true;
144-
// Count staged files
145145
try {
146146
const stagedFiles = execSync('git diff --cached --name-only', {
147147
cwd: gitRoot,
148148
encoding: 'utf-8',
149+
stdio: 'pipe',
149150
});
150151
stagedFileCount = stagedFiles.trim().split('\n').filter(Boolean).length;
151152
} catch {
@@ -157,14 +158,15 @@ export class ReviewAgent {
157158
execSync('git diff --quiet', {
158159
cwd: gitRoot,
159160
encoding: 'utf-8',
161+
stdio: 'pipe',
160162
});
161163
} catch {
162164
hasUnstaged = true;
163-
// Count unstaged files
164165
try {
165166
const unstagedFiles = execSync('git diff --name-only', {
166167
cwd: gitRoot,
167168
encoding: 'utf-8',
169+
stdio: 'pipe',
168170
});
169171
unstagedFileCount = unstagedFiles
170172
.trim()
@@ -189,6 +191,7 @@ export class ReviewAgent {
189191
cwd: gitRoot,
190192
encoding: 'utf-8',
191193
maxBuffer: 10 * 1024 * 1024,
194+
stdio: 'pipe',
192195
});
193196

194197
if (!stagedDiff) {
@@ -216,6 +219,7 @@ export class ReviewAgent {
216219
cwd: gitRoot,
217220
encoding: 'utf-8',
218221
maxBuffer: 10 * 1024 * 1024,
222+
stdio: 'pipe',
219223
});
220224

221225
if (!unstagedDiff) {
@@ -239,18 +243,18 @@ export class ReviewAgent {
239243
*/
240244
getGitDiff(gitRoot: string): string {
241245
try {
242-
// Get staged changes
243246
const stagedDiff = execSync('git diff --cached', {
244247
cwd: gitRoot,
245248
encoding: 'utf-8',
246-
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
249+
maxBuffer: 10 * 1024 * 1024,
250+
stdio: 'pipe',
247251
});
248252

249-
// Get unstaged changes
250253
const unstagedDiff = execSync('git diff', {
251254
cwd: gitRoot,
252255
encoding: 'utf-8',
253256
maxBuffer: 10 * 1024 * 1024,
257+
stdio: 'pipe',
254258
});
255259

256260
// Combine both diffs

0 commit comments

Comments
 (0)