@aion0/forge 0.5.44 → 0.5.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/RELEASE_NOTES.md +3 -15
  2. package/package.json +1 -1
  3. package/tsconfig.json +3 -1
  4. package/intellij-plugin/README.md +0 -53
  5. package/intellij-plugin/build.gradle.kts +0 -60
  6. package/intellij-plugin/gradle/gradle-daemon-jvm.properties +0 -12
  7. package/intellij-plugin/gradle.properties +0 -9
  8. package/intellij-plugin/publish.sh +0 -78
  9. package/intellij-plugin/settings.gradle.kts +0 -7
  10. package/intellij-plugin/src/main/kotlin/com/aion0/forge/action/LoginAction.kt +0 -49
  11. package/intellij-plugin/src/main/kotlin/com/aion0/forge/action/LogoutAction.kt +0 -18
  12. package/intellij-plugin/src/main/kotlin/com/aion0/forge/action/OpenWebUIAction.kt +0 -13
  13. package/intellij-plugin/src/main/kotlin/com/aion0/forge/action/SwitchConnectionAction.kt +0 -26
  14. package/intellij-plugin/src/main/kotlin/com/aion0/forge/api/ForgeClient.kt +0 -115
  15. package/intellij-plugin/src/main/kotlin/com/aion0/forge/auth/Auth.kt +0 -31
  16. package/intellij-plugin/src/main/kotlin/com/aion0/forge/connection/ConnectionManager.kt +0 -95
  17. package/intellij-plugin/src/main/kotlin/com/aion0/forge/settings/ForgeConfigurable.kt +0 -81
  18. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/DocsView.kt +0 -99
  19. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/ForgeStatusBarWidgetFactory.kt +0 -94
  20. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/ForgeToolWindowFactory.kt +0 -27
  21. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/ForgeTreeView.kt +0 -176
  22. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/Helpers.kt +0 -48
  23. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/PipelinesView.kt +0 -226
  24. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/TerminalsView.kt +0 -309
  25. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/TreeNodeData.kt +0 -33
  26. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/WorkspacesView.kt +0 -166
  27. package/intellij-plugin/src/main/resources/META-INF/plugin.xml +0 -88
  28. package/intellij-plugin/src/main/resources/icons/forge.svg +0 -3
  29. package/vscode-extension/.vscodeignore +0 -11
  30. package/vscode-extension/README.md +0 -48
  31. package/vscode-extension/media/icon.png +0 -0
  32. package/vscode-extension/media/icon.svg +0 -3
  33. package/vscode-extension/package-lock.json +0 -4046
  34. package/vscode-extension/package.json +0 -514
  35. package/vscode-extension/publish.sh +0 -49
  36. package/vscode-extension/src/api/client.ts +0 -217
  37. package/vscode-extension/src/auth/auth.ts +0 -32
  38. package/vscode-extension/src/commands/auth.ts +0 -44
  39. package/vscode-extension/src/commands/connection.ts +0 -113
  40. package/vscode-extension/src/commands/docs.ts +0 -40
  41. package/vscode-extension/src/commands/pipeline.ts +0 -103
  42. package/vscode-extension/src/commands/server.ts +0 -50
  43. package/vscode-extension/src/commands/smith.ts +0 -112
  44. package/vscode-extension/src/commands/task.ts +0 -43
  45. package/vscode-extension/src/commands/terminal.ts +0 -279
  46. package/vscode-extension/src/commands/workspace.ts +0 -138
  47. package/vscode-extension/src/connection/manager.ts +0 -80
  48. package/vscode-extension/src/docs/fs-provider.ts +0 -94
  49. package/vscode-extension/src/docs/result-provider.ts +0 -33
  50. package/vscode-extension/src/docs/transport.ts +0 -22
  51. package/vscode-extension/src/extension.ts +0 -314
  52. package/vscode-extension/src/statusbar.ts +0 -70
  53. package/vscode-extension/src/terminal/pseudoterm.ts +0 -123
  54. package/vscode-extension/src/views/docs.ts +0 -145
  55. package/vscode-extension/src/views/pipelines.ts +0 -222
  56. package/vscode-extension/src/views/terminals.ts +0 -91
  57. package/vscode-extension/src/views/workspaces.ts +0 -243
  58. package/vscode-extension/tsconfig.json +0 -16
@@ -1,81 +0,0 @@
1
- package com.aion0.forge.settings
2
-
3
- import com.aion0.forge.connection.ConnectionManager
4
- import com.aion0.forge.connection.ForgeConnection
5
- import com.intellij.openapi.options.Configurable
6
- import com.intellij.ui.ToolbarDecorator
7
- import com.intellij.ui.table.JBTable
8
- import javax.swing.JComponent
9
- import javax.swing.JPanel
10
- import javax.swing.JTextField
11
- import javax.swing.table.DefaultTableModel
12
-
13
- /** "Tools → Forge" preferences pane: edit the list of connections and pick the
14
- * active one. Tokens are stored separately in PasswordSafe (cleared via the
15
- * Forge: Logout action). */
16
- class ForgeConfigurable : Configurable {
17
- private lateinit var rootPanel: JPanel
18
- private lateinit var activeNameField: JTextField
19
- private lateinit var tableModel: DefaultTableModel
20
- private lateinit var table: JBTable
21
-
22
- override fun getDisplayName(): String = "Forge"
23
-
24
- override fun createComponent(): JComponent {
25
- tableModel = object : DefaultTableModel(arrayOf("Name", "Server URL", "Terminal URL"), 0) {
26
- override fun isCellEditable(row: Int, col: Int) = true
27
- }
28
- table = JBTable(tableModel)
29
-
30
- for (c in ConnectionManager.get().list()) {
31
- tableModel.addRow(arrayOf(c.name, c.serverUrl, c.terminalUrl))
32
- }
33
-
34
- val tableWithToolbar = ToolbarDecorator.createDecorator(table)
35
- .setAddAction { tableModel.addRow(arrayOf("New", "http://localhost:8403", "ws://localhost:8404")) }
36
- .setRemoveAction {
37
- val sel = table.selectedRow
38
- if (sel >= 0 && tableModel.rowCount > 1) tableModel.removeRow(sel)
39
- }
40
- .createPanel()
41
-
42
- activeNameField = JTextField(ConnectionManager.get().active().name)
43
-
44
- rootPanel = JPanel().apply {
45
- layout = javax.swing.BoxLayout(this, javax.swing.BoxLayout.Y_AXIS)
46
- add(javax.swing.JLabel("Active connection name:"))
47
- add(activeNameField)
48
- add(javax.swing.Box.createVerticalStrut(8))
49
- add(javax.swing.JLabel("Saved connections:"))
50
- add(tableWithToolbar)
51
- }
52
- return rootPanel
53
- }
54
-
55
- override fun isModified(): Boolean {
56
- val current = collectFromTable()
57
- return current != ConnectionManager.get().list() ||
58
- activeNameField.text != ConnectionManager.get().active().name
59
- }
60
-
61
- override fun apply() {
62
- val list = collectFromTable()
63
- ConnectionManager.get().replaceAll(list, activeNameField.text.trim())
64
- }
65
-
66
- override fun reset() {
67
- tableModel.rowCount = 0
68
- for (c in ConnectionManager.get().list()) {
69
- tableModel.addRow(arrayOf(c.name, c.serverUrl, c.terminalUrl))
70
- }
71
- activeNameField.text = ConnectionManager.get().active().name
72
- }
73
-
74
- private fun collectFromTable(): List<ForgeConnection> = (0 until tableModel.rowCount).map { i ->
75
- ForgeConnection(
76
- name = (tableModel.getValueAt(i, 0) ?: "").toString().trim(),
77
- serverUrl = (tableModel.getValueAt(i, 1) ?: "").toString().trim(),
78
- terminalUrl = (tableModel.getValueAt(i, 2) ?: "").toString().trim(),
79
- )
80
- }.filter { it.name.isNotEmpty() }
81
- }
@@ -1,99 +0,0 @@
1
- package com.aion0.forge.ui.toolwindow
2
-
3
- import com.aion0.forge.api.ForgeClient
4
- import com.google.gson.JsonElement
5
- import com.intellij.icons.AllIcons
6
- import com.intellij.openapi.actionSystem.AnAction
7
- import com.intellij.openapi.actionSystem.AnActionEvent
8
- import com.intellij.openapi.fileEditor.FileEditorManager
9
- import com.intellij.openapi.project.Project
10
- import com.intellij.openapi.vfs.LocalFileSystem
11
- import org.jetbrains.plugins.terminal.TerminalView
12
- import java.io.File
13
- import javax.swing.tree.DefaultMutableTreeNode
14
-
15
- class DocsView(project: Project) : ForgeTreeView(project) {
16
-
17
- override fun rootLabel() = "docs"
18
-
19
- override fun reload(): List<DefaultMutableTreeNode> {
20
- val r = ForgeClient.get().request("/api/docs")
21
- if (r.status == 401 || r.status == 403) return listOf(DefaultMutableTreeNode(TreeNodeData.Hint("🔑 Tools → Forge: Login")))
22
- if (!r.ok || r.data == null || !r.data.isJsonObject) {
23
- return listOf(DefaultMutableTreeNode(TreeNodeData.Hint("⚠ ${r.error ?: "Not connected"}")))
24
- }
25
- val roots = r.data.asJsonObject.getAsJsonArray("roots") ?: return listOf(DefaultMutableTreeNode(TreeNodeData.Hint("No doc roots — add one in forge Settings → Doc Roots")))
26
- val rootPaths = r.data.asJsonObject.getAsJsonArray("rootPaths")
27
- if (roots.size() == 0) return listOf(DefaultMutableTreeNode(TreeNodeData.Hint("No doc roots")))
28
-
29
- return (0 until roots.size()).map { i ->
30
- val rootName = roots[i].asString
31
- val rootPath = rootPaths?.get(i)?.asString ?: rootName
32
- val node = DefaultMutableTreeNode(TreeNodeData.DocRoot("📚 $rootName", i, rootPath, rootName))
33
- val sub = ForgeClient.get().request("/api/docs?root=$i")
34
- sub.data?.asJsonObject?.getAsJsonArray("tree")?.forEach { addChildNode(node, it, i, rootPath) }
35
- node
36
- }
37
- }
38
-
39
- private fun addChildNode(parent: DefaultMutableTreeNode, el: JsonElement, rootIdx: Int, rootPath: String) {
40
- val obj = el.asJsonObject
41
- val name = obj.get("name")?.asString ?: "?"
42
- val type = obj.get("type")?.asString ?: "file"
43
- val relPath = obj.get("path")?.asString ?: name
44
- val fileType = obj.get("fileType")?.asString
45
- val emoji = when {
46
- type == "dir" -> "📁"
47
- fileType == "md" -> "📄"
48
- fileType == "image" -> "🖼"
49
- else -> "📑"
50
- }
51
- val data = if (type == "dir") TreeNodeData.DocDir("$emoji $name", rootIdx, rootPath, relPath)
52
- else TreeNodeData.DocFile("$emoji $name", rootIdx, rootPath, relPath, fileType)
53
- val node = DefaultMutableTreeNode(data)
54
- if (type == "dir") obj.getAsJsonArray("children")?.forEach { addChildNode(node, it, rootIdx, rootPath) }
55
- parent.add(node)
56
- }
57
-
58
- override fun onDoubleClick(data: TreeNodeData, node: DefaultMutableTreeNode) {
59
- when (data) {
60
- is TreeNodeData.DocFile -> openFile(data)
61
- else -> {}
62
- }
63
- }
64
-
65
- override fun contextActions(data: TreeNodeData, node: DefaultMutableTreeNode): List<AnAction> = when (data) {
66
- is TreeNodeData.DocRoot -> listOf(
67
- act("Open Terminal Here", AllIcons.Debugger.Console) { openTerminalAt(data.rootPath) },
68
- )
69
- is TreeNodeData.DocDir -> listOf(
70
- act("Open Terminal Here", AllIcons.Debugger.Console) { openTerminalAt("${data.rootPath}/${data.relPath}") },
71
- )
72
- is TreeNodeData.DocFile -> listOf(
73
- act("Open", AllIcons.Actions.Edit) { openFile(data) },
74
- )
75
- else -> emptyList()
76
- }
77
-
78
- /** Open the file via the local filesystem (forge is local-by-default —
79
- * remote forges would need an HTTP-backed VFS, deferred). */
80
- private fun openFile(f: TreeNodeData.DocFile) {
81
- val abs = File(f.rootPath, f.relPath)
82
- if (!abs.isFile) {
83
- notify(project, "Forge: file not found locally — ${abs.absolutePath} (remote forge support TBD)", com.intellij.notification.NotificationType.WARNING)
84
- return
85
- }
86
- val vf = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(abs) ?: return
87
- FileEditorManager.getInstance(project).openFile(vf, true)
88
- }
89
-
90
- private fun openTerminalAt(absDir: String) {
91
- val terminalView = TerminalView.getInstance(project)
92
- val widget = terminalView.createLocalShellWidget(absDir, "forge: ${File(absDir).name}")
93
- widget.executeCommand("claude --dangerously-skip-permissions")
94
- }
95
-
96
- private fun act(name: String, icon: javax.swing.Icon?, run: () -> Unit) = object : AnAction(name, null, icon) {
97
- override fun actionPerformed(e: AnActionEvent) = run()
98
- }
99
- }
@@ -1,94 +0,0 @@
1
- package com.aion0.forge.ui.toolwindow
2
-
3
- import com.aion0.forge.api.ForgeClient
4
- import com.aion0.forge.auth.Auth
5
- import com.aion0.forge.connection.ConnectionListener
6
- import com.aion0.forge.connection.ConnectionManager
7
- import com.aion0.forge.connection.ForgeConnection
8
- import com.intellij.openapi.actionSystem.ActionManager
9
- import com.intellij.openapi.application.ApplicationManager
10
- import com.intellij.openapi.project.Project
11
- import com.intellij.openapi.wm.StatusBar
12
- import com.intellij.openapi.wm.StatusBarWidget
13
- import com.intellij.openapi.wm.StatusBarWidgetFactory
14
- import com.intellij.util.Alarm
15
- import java.awt.event.MouseEvent
16
- import javax.swing.Timer
17
-
18
- class ForgeStatusBarWidgetFactory : StatusBarWidgetFactory {
19
- override fun getId(): String = "com.aion0.forge.statusbar"
20
- override fun getDisplayName(): String = "Forge"
21
- override fun isAvailable(project: Project): Boolean = true
22
- override fun createWidget(project: Project): StatusBarWidget = ForgeStatusBarWidget(project)
23
- override fun disposeWidget(widget: StatusBarWidget) {}
24
- override fun canBeEnabledOn(statusBar: StatusBar): Boolean = true
25
- }
26
-
27
- class ForgeStatusBarWidget(private val project: Project) : StatusBarWidget,
28
- StatusBarWidget.TextPresentation {
29
-
30
- private var statusBar: StatusBar? = null
31
- // `this` is a Disposable (StatusBarWidget extends Disposable). The Alarm
32
- // requires a parent Disposable for non-Swing threads — registering with
33
- // ourselves means the Alarm is auto-disposed when the widget goes away.
34
- private val alarm: Alarm by lazy { Alarm(Alarm.ThreadToUse.POOLED_THREAD, this) }
35
- private var lastText: String = "Forge: …"
36
- private var lastTooltip: String = "Forge"
37
- private var pollTimer: Timer? = null
38
-
39
- override fun ID(): String = "com.aion0.forge.statusbar"
40
- override fun getPresentation(): StatusBarWidget.WidgetPresentation = this
41
-
42
- override fun install(statusBar: StatusBar) {
43
- this.statusBar = statusBar
44
- // Refresh on connection change.
45
- ApplicationManager.getApplication().messageBus.connect()
46
- .subscribe(ConnectionListener.TOPIC, object : ConnectionListener {
47
- override fun onConnectionChanged(active: ForgeConnection) = scheduleRefresh()
48
- })
49
- scheduleRefresh()
50
- // Periodic poll so connectivity changes (server up/down) reflect.
51
- pollTimer = Timer(5_000) { scheduleRefresh() }.apply { start() }
52
- }
53
-
54
- override fun dispose() {
55
- pollTimer?.stop()
56
- pollTimer = null
57
- // alarm is disposed automatically (we passed `this` as parent).
58
- }
59
-
60
- override fun getText(): String = lastText
61
- override fun getTooltipText(): String = lastTooltip
62
- override fun getAlignment(): Float = 0f
63
-
64
- override fun getClickConsumer(): com.intellij.util.Consumer<MouseEvent>? =
65
- com.intellij.util.Consumer { _ ->
66
- // Click → run "Switch Connection" action.
67
- val action = ActionManager.getInstance().getAction("com.aion0.forge.action.SwitchConnectionAction")
68
- ActionManager.getInstance().tryToExecute(action, null, null, "Forge", true)
69
- }
70
-
71
- private fun scheduleRefresh() {
72
- alarm.cancelAllRequests()
73
- alarm.addRequest({
74
- val conn = ConnectionManager.get().active()
75
- val reachable = ForgeClient.get().ping()
76
- val token = Auth.get().getToken(conn.name)
77
- val text = when {
78
- !reachable -> "Forge ⊘ ${conn.name}"
79
- token.isNullOrBlank() -> "Forge ⚠ ${conn.name}"
80
- else -> "Forge ⚡ ${conn.name}"
81
- }
82
- val tooltip = when {
83
- !reachable -> "${conn.name} — server unreachable (${conn.serverUrl})"
84
- token.isNullOrBlank() -> "${conn.name} — login required"
85
- else -> "${conn.name} — connected (${conn.serverUrl})"
86
- }
87
- ApplicationManager.getApplication().invokeLater {
88
- lastText = text
89
- lastTooltip = tooltip
90
- statusBar?.updateWidget(ID())
91
- }
92
- }, 100)
93
- }
94
- }
@@ -1,27 +0,0 @@
1
- package com.aion0.forge.ui.toolwindow
2
-
3
- import com.intellij.openapi.project.DumbAware
4
- import com.intellij.openapi.project.Project
5
- import com.intellij.openapi.util.Disposer
6
- import com.intellij.openapi.wm.ToolWindow
7
- import com.intellij.openapi.wm.ToolWindowFactory
8
- import com.intellij.ui.content.Content
9
- import com.intellij.ui.content.ContentFactory
10
-
11
- class ForgeToolWindowFactory : ToolWindowFactory, DumbAware {
12
- override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
13
- val cf = ContentFactory.getInstance()
14
- addView(toolWindow, cf, "Workspaces", WorkspacesView(project))
15
- addView(toolWindow, cf, "Terminals", TerminalsView(project))
16
- addView(toolWindow, cf, "Pipelines", PipelinesView(project))
17
- addView(toolWindow, cf, "Docs", DocsView(project))
18
- }
19
-
20
- private fun addView(tw: ToolWindow, cf: ContentFactory, name: String, view: ForgeTreeView) {
21
- val content: Content = cf.createContent(view.component(), name, false)
22
- // Tie the view's lifecycle to the tab content so background polling
23
- // stops when the user closes the tool window.
24
- Disposer.register(content, view)
25
- tw.contentManager.addContent(content)
26
- }
27
- }
@@ -1,176 +0,0 @@
1
- package com.aion0.forge.ui.toolwindow
2
-
3
- import com.aion0.forge.connection.ConnectionListener
4
- import com.aion0.forge.connection.ForgeConnection
5
- import com.intellij.openapi.Disposable
6
- import com.intellij.openapi.actionSystem.ActionManager
7
- import com.intellij.openapi.actionSystem.AnAction
8
- import com.intellij.openapi.actionSystem.AnActionEvent
9
- import com.intellij.openapi.actionSystem.DefaultActionGroup
10
- import com.intellij.openapi.application.ApplicationManager
11
- import com.intellij.openapi.project.Project
12
- import com.intellij.openapi.util.Disposer
13
- import com.intellij.ui.PopupHandler
14
- import com.intellij.ui.ScrollPaneFactory
15
- import com.intellij.ui.treeStructure.Tree
16
- import com.intellij.util.Alarm
17
- import java.awt.BorderLayout
18
- import java.awt.event.MouseAdapter
19
- import java.awt.event.MouseEvent
20
- import javax.swing.JPanel
21
- import javax.swing.tree.DefaultMutableTreeNode
22
- import javax.swing.tree.DefaultTreeModel
23
- import javax.swing.tree.TreePath
24
-
25
- /**
26
- * Common boilerplate for the four Forge tool-window tabs.
27
- *
28
- * - Tree component wrapped in a scroll pane
29
- * - Toolbar with a refresh button
30
- * - Auto-refresh every 5 s
31
- * - Refresh on active-connection change
32
- *
33
- * Subclasses override [reload] to populate the [root] node and [refreshUi].
34
- */
35
- abstract class ForgeTreeView(protected val project: Project) : Disposable {
36
- protected val root: DefaultMutableTreeNode = DefaultMutableTreeNode(rootLabel())
37
- protected val treeModel = DefaultTreeModel(root)
38
- protected val tree = Tree(treeModel).apply {
39
- isRootVisible = false
40
- showsRootHandles = true
41
- }
42
- private val alarm = Alarm(Alarm.ThreadToUse.POOLED_THREAD, this)
43
-
44
- /** Build the panel — wrap toolbar + tree in a JPanel. */
45
- fun component(): JPanel {
46
- val panel = JPanel(BorderLayout())
47
- // Toolbar
48
- val actions = DefaultActionGroup().apply {
49
- add(RefreshAction())
50
- toolbarActions(this)
51
- }
52
- val toolbar = ActionManager.getInstance().createActionToolbar("ForgeView", actions, true)
53
- toolbar.targetComponent = tree
54
- panel.add(toolbar.component, BorderLayout.NORTH)
55
- // Tree
56
- panel.add(ScrollPaneFactory.createScrollPane(tree), BorderLayout.CENTER)
57
- // Click + right-click handlers — subclasses override.
58
- tree.addMouseListener(object : MouseAdapter() {
59
- override fun mouseClicked(e: MouseEvent) {
60
- if (e.clickCount == 2 && !e.isPopupTrigger) {
61
- val path = tree.getPathForLocation(e.x, e.y) ?: return
62
- val node = path.lastPathComponent as? DefaultMutableTreeNode ?: return
63
- val data = node.userObject as? TreeNodeData ?: return
64
- onDoubleClick(data, node)
65
- }
66
- }
67
- })
68
- PopupHandler.installPopupMenu(tree, object : DefaultActionGroup() {
69
- override fun getChildren(e: AnActionEvent?): Array<AnAction> {
70
- val node = selectedNode() ?: return emptyArray()
71
- val data = node.userObject as? TreeNodeData ?: return emptyArray()
72
- return contextActions(data, node).toTypedArray()
73
- }
74
- }, "ForgeViewPopup")
75
-
76
- // Listen for connection changes → reload.
77
- ApplicationManager.getApplication().messageBus.connect(this)
78
- .subscribe(ConnectionListener.TOPIC, object : ConnectionListener {
79
- override fun onConnectionChanged(active: ForgeConnection) = scheduleReload()
80
- })
81
-
82
- scheduleReload()
83
- // Periodic 5s refresh.
84
- startPolling()
85
- return panel
86
- }
87
-
88
- private fun startPolling() {
89
- // Tail-recursive scheduling so we always wait 5s after each completion.
90
- if (!Disposer.isDisposed(this)) {
91
- alarm.addRequest({
92
- doReloadAsync()
93
- startPolling()
94
- }, 5_000)
95
- }
96
- }
97
-
98
- private fun scheduleReload() {
99
- alarm.cancelAllRequests()
100
- alarm.addRequest({ doReloadAsync() }, 50)
101
- }
102
-
103
- private fun doReloadAsync() {
104
- val newChildren = runCatching { reload() }.getOrElse {
105
- listOf(DefaultMutableTreeNode("⚠ ${it.message ?: "error"}"))
106
- }
107
- ApplicationManager.getApplication().invokeLater {
108
- // Snapshot the userObject-string path of every expanded node so we can
109
- // re-expand them after the tree is rebuilt — otherwise the 5s poll
110
- // collapses anything the user opened (Docs subfolders, Pipeline runs, …).
111
- val expanded = collectExpandedPaths()
112
- root.removeAllChildren()
113
- for (n in newChildren) root.add(n)
114
- treeModel.reload(root)
115
- reExpand(expanded)
116
- // Continue polling unless we've been disposed.
117
- if (Disposer.isDisposed(this)) alarm.cancelAllRequests()
118
- }
119
- }
120
-
121
- /** Path identity of every expanded node, keyed by the chain of userObject
122
- * toStrings from root → leaf (root itself excluded). */
123
- private fun collectExpandedPaths(): Set<List<String>> {
124
- val out = mutableSetOf<List<String>>()
125
- val descendants = tree.getExpandedDescendants(TreePath(root.path)) ?: return out
126
- while (descendants.hasMoreElements()) {
127
- val tp = descendants.nextElement()
128
- val parts = tp.path
129
- if (parts.size <= 1) continue
130
- out.add((1 until parts.size).map { (parts[it] as DefaultMutableTreeNode).userObject?.toString().orEmpty() })
131
- }
132
- return out
133
- }
134
-
135
- private fun reExpand(expanded: Set<List<String>>) {
136
- if (expanded.isEmpty()) return
137
- fun visit(node: DefaultMutableTreeNode, prefix: List<String>) {
138
- for (i in 0 until node.childCount) {
139
- val child = node.getChildAt(i) as DefaultMutableTreeNode
140
- val key = prefix + (child.userObject?.toString().orEmpty())
141
- if (key in expanded) tree.expandPath(TreePath(child.path))
142
- visit(child, key)
143
- }
144
- }
145
- visit(root, emptyList())
146
- }
147
-
148
- /** Build the children of the synthetic root (top-level rows the user sees). */
149
- protected abstract fun reload(): List<DefaultMutableTreeNode>
150
-
151
- /** Override to add view-specific buttons to the toolbar (after Refresh). */
152
- protected open fun toolbarActions(group: DefaultActionGroup) {}
153
-
154
- /** Override to handle a left double-click on a typed tree node. */
155
- protected open fun onDoubleClick(data: TreeNodeData, node: DefaultMutableTreeNode) {}
156
-
157
- /** Override to add right-click menu items for a typed tree node. */
158
- protected open fun contextActions(data: TreeNodeData, node: DefaultMutableTreeNode): List<AnAction> = emptyList()
159
-
160
- /** Label for the hidden synthetic root — never shown but useful as a key. */
161
- protected open fun rootLabel(): String = "ROOT"
162
-
163
- /** Trigger a refresh from subclasses (e.g. after running an action). */
164
- fun refresh() = scheduleReload()
165
-
166
- protected fun selectedNode(): DefaultMutableTreeNode? =
167
- tree.lastSelectedPathComponent as? DefaultMutableTreeNode
168
-
169
- override fun dispose() {
170
- alarm.cancelAllRequests()
171
- }
172
-
173
- private inner class RefreshAction : AnAction("Refresh", "Refresh from Forge", com.intellij.icons.AllIcons.Actions.Refresh) {
174
- override fun actionPerformed(e: AnActionEvent) = scheduleReload()
175
- }
176
- }
@@ -1,48 +0,0 @@
1
- package com.aion0.forge.ui.toolwindow
2
-
3
- import com.aion0.forge.api.ApiResult
4
- import com.aion0.forge.api.ForgeClient
5
- import com.intellij.notification.NotificationGroupManager
6
- import com.intellij.notification.NotificationType
7
- import com.intellij.openapi.application.ApplicationManager
8
- import com.intellij.openapi.progress.ProgressIndicator
9
- import com.intellij.openapi.progress.ProgressManager
10
- import com.intellij.openapi.progress.Task
11
- import com.intellij.openapi.project.Project
12
-
13
- internal fun notify(project: Project?, message: String, type: NotificationType = NotificationType.INFORMATION) {
14
- NotificationGroupManager.getInstance().getNotificationGroup("Forge")
15
- .createNotification(message, type)
16
- .notify(project)
17
- }
18
-
19
- internal fun runBg(project: Project?, title: String, work: () -> Unit) {
20
- ProgressManager.getInstance().run(object : Task.Backgroundable(project, title, false) {
21
- override fun run(indicator: ProgressIndicator) { work() }
22
- })
23
- }
24
-
25
- /** Convenience: run an API call on a bg thread, then show a success/failure
26
- * notification on EDT, then call onSuccess (used to trigger view refreshes). */
27
- internal fun runApi(project: Project?, title: String, call: () -> ApiResult, onSuccess: () -> Unit = {}) {
28
- runBg(project, title) {
29
- val res = call()
30
- ApplicationManager.getApplication().invokeLater {
31
- if (res.ok) {
32
- notify(project, "Forge: $title — ok")
33
- onSuccess()
34
- } else {
35
- notify(project, "Forge: $title failed — ${res.error}", NotificationType.ERROR)
36
- }
37
- }
38
- }
39
- }
40
-
41
- /** WS action against the workspace daemon: POST /api/workspace/<id>/agents
42
- * with `{action, agentId, ...}`. */
43
- internal fun wsAction(workspaceId: String, action: String, body: Map<String, Any> = emptyMap()): ApiResult =
44
- ForgeClient.get().request(
45
- "/api/workspace/$workspaceId/agents",
46
- method = "POST",
47
- body = mapOf("action" to action) + body,
48
- )