@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,226 +0,0 @@
1
- package com.aion0.forge.ui.toolwindow
2
-
3
- import com.aion0.forge.api.ForgeClient
4
- import com.intellij.icons.AllIcons
5
- import com.intellij.openapi.actionSystem.AnAction
6
- import com.intellij.openapi.actionSystem.AnActionEvent
7
- import com.intellij.openapi.fileEditor.FileEditorManager
8
- import com.intellij.openapi.project.Project
9
- import com.intellij.openapi.ui.Messages
10
- import com.intellij.testFramework.LightVirtualFile
11
- import java.net.URLEncoder
12
- import javax.swing.tree.DefaultMutableTreeNode
13
-
14
- class PipelinesView(project: Project) : ForgeTreeView(project) {
15
-
16
- override fun rootLabel() = "pipelines"
17
-
18
- override fun reload(): List<DefaultMutableTreeNode> {
19
- val r = ForgeClient.get().request("/api/projects")
20
- if (r.status == 401 || r.status == 403) return listOf(DefaultMutableTreeNode(TreeNodeData.Hint("πŸ”‘ Tools β†’ Forge: Login")))
21
- if (!r.ok || r.data == null || !r.data.isJsonArray) {
22
- return listOf(DefaultMutableTreeNode(TreeNodeData.Hint("⚠ ${r.error ?: "Not connected"}")))
23
- }
24
- val arr = r.data.asJsonArray
25
- if (arr.size() == 0) return listOf(DefaultMutableTreeNode(TreeNodeData.Hint("No projects")))
26
-
27
- return arr.toList().sortedBy { it.asJsonObject.get("name")?.asString ?: "" }.mapNotNull { el ->
28
- val p = el.asJsonObject
29
- val name = p.get("name")?.asString ?: return@mapNotNull null
30
- val path = p.get("path")?.asString ?: return@mapNotNull null
31
- val node = DefaultMutableTreeNode(TreeNodeData.PipelineProject("πŸ“ $name", path, name))
32
-
33
- val pp = ForgeClient.get().request("/api/project-pipelines?project=${URLEncoder.encode(path, "UTF-8")}")
34
- if (pp.ok && pp.data?.isJsonObject == true) {
35
- val obj = pp.data.asJsonObject
36
- val bindings = obj.getAsJsonArray("bindings")
37
- val runs = obj.getAsJsonArray("runs")
38
- if (bindings == null || bindings.size() == 0) {
39
- node.add(DefaultMutableTreeNode(TreeNodeData.Hint("οΌ‹ No pipelines yet β€” right-click to add")))
40
- } else {
41
- for (bEl in bindings) {
42
- val b = bEl.asJsonObject
43
- val wf = b.get("workflowName")?.asString ?: "?"
44
- val enabled = b.get("enabled")?.asBoolean ?: true
45
- val schedule = b.getAsJsonObject("config")?.get("schedule")?.asString
46
- ?: b.getAsJsonObject("config")?.get("cron")?.asString
47
- ?: "manual"
48
- val mark = if (enabled) "βš™" else "⊘"
49
- node.add(DefaultMutableTreeNode(
50
- TreeNodeData.PipelineBinding("$mark $wf ($schedule)", path, name, wf, enabled),
51
- ))
52
- }
53
- }
54
- if (runs != null && runs.size() > 0) {
55
- val runsHead = DefaultMutableTreeNode(TreeNodeData.Hint("πŸ“œ Recent Runs"))
56
- runs.take(10).forEach { rEl ->
57
- val run = rEl.asJsonObject
58
- val status = run.get("status")?.asString ?: "?"
59
- val wf = run.get("workflowName")?.asString ?: "?"
60
- val pipelineId = run.get("pipelineId")?.asString ?: run.get("id")?.asString ?: ""
61
- val createdAt = run.get("createdAt")?.asString ?: ""
62
- val emoji = when (status) {
63
- "running" -> "β–Ά"; "done" -> "βœ“"; "failed" -> "βœ•"; "cancelled" -> "⊘"; else -> "Β·"
64
- }
65
- runsHead.add(DefaultMutableTreeNode(
66
- TreeNodeData.PipelineRun("$emoji $wf Β· $createdAt", pipelineId, wf, status),
67
- ))
68
- }
69
- node.add(runsHead)
70
- }
71
- }
72
- node
73
- }
74
- }
75
-
76
- override fun onDoubleClick(data: TreeNodeData, node: DefaultMutableTreeNode) {
77
- when (data) {
78
- is TreeNodeData.PipelineBinding -> triggerBinding(data)
79
- is TreeNodeData.PipelineRun -> expandRun(node, data)
80
- is TreeNodeData.PipelineNode -> showNodeResult(data)
81
- else -> {}
82
- }
83
- }
84
-
85
- override fun contextActions(data: TreeNodeData, node: DefaultMutableTreeNode): List<AnAction> = when (data) {
86
- is TreeNodeData.PipelineProject -> listOf(
87
- act("Add Pipeline…", AllIcons.General.Add) { addPipeline(data) },
88
- )
89
- is TreeNodeData.PipelineBinding -> listOf(
90
- act("Trigger Now", AllIcons.Actions.Execute) { triggerBinding(data) },
91
- act(if (data.enabled) "Disable" else "Enable", AllIcons.Actions.ToggleSoftWrap) { toggleBinding(data) },
92
- act("Remove…", AllIcons.Actions.Cancel) { removeBinding(data) },
93
- )
94
- is TreeNodeData.PipelineRun -> listOf(
95
- act("Show Nodes", AllIcons.Actions.Expandall) { expandRun(node, data) },
96
- )
97
- is TreeNodeData.PipelineNode -> listOf(
98
- act("Show Result", AllIcons.Actions.Preview) { showNodeResult(data) },
99
- )
100
- else -> emptyList()
101
- }
102
-
103
- private fun triggerBinding(b: TreeNodeData.PipelineBinding) {
104
- runApi(project, "Trigger ${b.workflowName}", {
105
- ForgeClient.get().request(
106
- "/api/project-pipelines",
107
- method = "POST",
108
- body = mapOf("action" to "trigger", "projectPath" to b.projectPath, "projectName" to b.projectName, "workflowName" to b.workflowName, "input" to emptyMap<String, String>()),
109
- )
110
- }) { refresh() }
111
- }
112
-
113
- private fun toggleBinding(b: TreeNodeData.PipelineBinding) {
114
- val next = !b.enabled
115
- runApi(project, if (next) "Enable ${b.workflowName}" else "Disable ${b.workflowName}", {
116
- ForgeClient.get().request(
117
- "/api/project-pipelines",
118
- method = "POST",
119
- body = mapOf("action" to "update", "projectPath" to b.projectPath, "workflowName" to b.workflowName, "enabled" to next),
120
- )
121
- }) { refresh() }
122
- }
123
-
124
- private fun removeBinding(b: TreeNodeData.PipelineBinding) {
125
- val r = Messages.showYesNoDialog(project, "Remove pipeline \"${b.workflowName}\"?", "Forge", Messages.getQuestionIcon())
126
- if (r != Messages.YES) return
127
- runApi(project, "Remove ${b.workflowName}", {
128
- ForgeClient.get().request(
129
- "/api/project-pipelines",
130
- method = "POST",
131
- body = mapOf("action" to "remove", "projectPath" to b.projectPath, "workflowName" to b.workflowName),
132
- )
133
- }) { refresh() }
134
- }
135
-
136
- private fun addPipeline(p: TreeNodeData.PipelineProject) {
137
- val pp = ForgeClient.get().request("/api/project-pipelines?project=${URLEncoder.encode(p.projectPath, "UTF-8")}")
138
- val workflows = pp.data?.asJsonObject?.getAsJsonArray("workflows")?.map { it.asJsonObject.get("name").asString } ?: return
139
- val bound = pp.data.asJsonObject.getAsJsonArray("bindings")?.map { it.asJsonObject.get("workflowName").asString }?.toSet() ?: emptySet()
140
- val candidates = workflows.filter { it !in bound }
141
- if (candidates.isEmpty()) { notify(project, "Forge: all workflows are already bound to ${p.projectName}"); return }
142
- val choice = Messages.showEditableChooseDialog("Workflow to bind", "Add Pipeline", null, candidates.toTypedArray(), candidates.first(), null) ?: return
143
- runApi(project, "Add $choice to ${p.projectName}", {
144
- ForgeClient.get().request(
145
- "/api/project-pipelines",
146
- method = "POST",
147
- body = mapOf("action" to "add", "projectPath" to p.projectPath, "projectName" to p.projectName, "workflowName" to choice, "config" to emptyMap<String, Any>()),
148
- )
149
- }) { refresh() }
150
- }
151
-
152
- private fun expandRun(parent: DefaultMutableTreeNode, run: TreeNodeData.PipelineRun) {
153
- if (run.pipelineId.isBlank()) { notify(project, "Forge: this run has no pipeline detail id."); return }
154
- val r = ForgeClient.get().request("/api/pipelines/${run.pipelineId}")
155
- if (!r.ok || r.data == null || !r.data.isJsonObject) {
156
- notify(project, "Forge: ${r.error ?: "failed to load run"}", com.intellij.notification.NotificationType.WARNING)
157
- return
158
- }
159
- val nodes = r.data.asJsonObject.getAsJsonObject("nodes")
160
- val order = r.data.asJsonObject.getAsJsonArray("nodeOrder")
161
- ?.map { it.asString } ?: nodes?.keySet()?.toList() ?: emptyList()
162
- com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater {
163
- parent.removeAllChildren()
164
- for (name in order) {
165
- val n = nodes?.getAsJsonObject(name) ?: continue
166
- val st = n.get("status")?.asString ?: "pending"
167
- val err = n.get("error")?.asString
168
- val taskId = n.get("taskId")?.asString
169
- val emoji = when (st) {
170
- "running" -> "β–Ά"; "done" -> "βœ“"; "failed" -> "βœ•"
171
- "cancelled" -> "⊘"; "skipped" -> "·"; else -> "·"
172
- }
173
- parent.add(DefaultMutableTreeNode(
174
- TreeNodeData.PipelineNode("$emoji $name ($st)", run.pipelineId, name, st, err, taskId),
175
- ))
176
- }
177
- treeModel.reload(parent)
178
- tree.expandPath(javax.swing.tree.TreePath(parent.path))
179
- }
180
- }
181
-
182
- /** Open a markdown buffer with status + error + (if available) the linked
183
- * task's prompt / result / log / git diff. */
184
- private fun showNodeResult(n: TreeNodeData.PipelineNode) {
185
- runBg(project, "Loading node result") {
186
- val sb = StringBuilder()
187
- sb.append("# Pipeline node: `${n.nodeName}`\n\n")
188
- sb.append("**Status:** ${n.status}\n")
189
- if (n.taskId != null) sb.append("**Task ID:** `${n.taskId}`\n")
190
- sb.append("\n")
191
-
192
- if (n.taskId != null) {
193
- val t = ForgeClient.get().request("/api/tasks/${n.taskId}")
194
- if (t.ok && t.data?.isJsonObject == true) {
195
- val task = t.data.asJsonObject
196
- task.get("prompt")?.asString?.let { sb.append("## Prompt\n```\n$it\n```\n\n") }
197
- task.get("resultSummary")?.asString?.let { sb.append("## Result\n$it\n\n") }
198
- task.get("gitDiff")?.asString?.let { sb.append("## Git Diff\n```diff\n$it\n```\n\n") }
199
- val log = task.getAsJsonArray("log")
200
- if (log != null && log.size() > 0) {
201
- sb.append("## Log (last 20)\n")
202
- log.toList().takeLast(20).forEach { e ->
203
- val o = e.asJsonObject
204
- val tag = o.get("subtype")?.asString ?: o.get("type")?.asString ?: "log"
205
- val content = o.get("content")?.asString ?: ""
206
- sb.append("- `[$tag]` ${content.take(500).replace("\n", " ")}\n")
207
- }
208
- sb.append("\n")
209
- }
210
- }
211
- }
212
- if (!n.error.isNullOrBlank()) sb.append("## Error\n```\n${n.error}\n```\n")
213
-
214
- val content = sb.toString()
215
- com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater {
216
- val vf = LightVirtualFile("forge-node-${n.nodeName}.md", content)
217
- vf.isWritable = false
218
- FileEditorManager.getInstance(project).openFile(vf, true)
219
- }
220
- }
221
- }
222
-
223
- private fun act(name: String, icon: javax.swing.Icon?, run: () -> Unit) = object : AnAction(name, null, icon) {
224
- override fun actionPerformed(e: AnActionEvent) = run()
225
- }
226
- }
@@ -1,309 +0,0 @@
1
- package com.aion0.forge.ui.toolwindow
2
-
3
- import com.aion0.forge.api.ForgeClient
4
- import com.intellij.icons.AllIcons
5
- import com.intellij.openapi.actionSystem.AnAction
6
- import com.intellij.openapi.actionSystem.AnActionEvent
7
- import com.intellij.openapi.actionSystem.DefaultActionGroup
8
- import com.intellij.openapi.application.ApplicationManager
9
- import com.intellij.openapi.project.Project
10
- import com.intellij.openapi.ui.Messages
11
- import org.jetbrains.plugins.terminal.LocalTerminalDirectRunner
12
- import org.jetbrains.plugins.terminal.ShellStartupOptions
13
- import org.jetbrains.plugins.terminal.TerminalTabState
14
- import org.jetbrains.plugins.terminal.TerminalToolWindowManager
15
- import java.net.URLEncoder
16
- import javax.swing.tree.DefaultMutableTreeNode
17
-
18
- /**
19
- * Project-keyed terminal launcher.
20
- *
21
- * Each top-level node is a forge project. Expand it to see its claude sessions
22
- * (most-recent first, bound session marked β˜…). Double-click a session resumes
23
- * exactly that session via `claude --resume <id>`. Right-click β†’ Open With
24
- * lists every configured agent for a fresh launch.
25
- *
26
- * Agents are spawned **directly as the pty process** via a custom
27
- * [LocalTerminalDirectRunner]: no shell, no `executeCommand`, so there is no
28
- * race with prompt-detection or shell startup that could swallow keystrokes.
29
- */
30
- class TerminalsView(project: Project) : ForgeTreeView(project) {
31
-
32
- /** Cached list of enabled agents β€” refreshed on every reload so the project
33
- * right-click submenu can list them synchronously. */
34
- @Volatile private var agentsCache: List<Triple<String, String, String?>> = emptyList()
35
-
36
- override fun rootLabel() = "terminals"
37
-
38
- override fun reload(): List<DefaultMutableTreeNode> {
39
- // Refresh agent cache for the right-click submenu (cheap; ignore failures).
40
- ForgeClient.get().request("/api/agents").let { ar ->
41
- if (ar.ok && ar.data?.isJsonObject == true) {
42
- agentsCache = ar.data.asJsonObject.getAsJsonArray("agents")?.toList()?.mapNotNull { el ->
43
- val o = el.asJsonObject
44
- if (o.get("enabled")?.asBoolean == false) return@mapNotNull null
45
- val id = o.get("id")?.asString ?: return@mapNotNull null
46
- val name = o.get("name")?.asString ?: id
47
- Triple(id, name, o.get("cliType")?.asString)
48
- }.orEmpty()
49
- }
50
- }
51
-
52
- val r = ForgeClient.get().request("/api/projects")
53
- if (r.status == 401 || r.status == 403) return listOf(DefaultMutableTreeNode(TreeNodeData.Hint("πŸ”‘ Tools β†’ Forge: Login")))
54
- if (!r.ok || r.data == null || !r.data.isJsonArray) {
55
- return listOf(DefaultMutableTreeNode(TreeNodeData.Hint("⚠ ${r.error ?: "Not connected"}")))
56
- }
57
- val arr = r.data.asJsonArray
58
- if (arr.size() == 0) return listOf(DefaultMutableTreeNode(TreeNodeData.Hint("No projects yet")))
59
-
60
- return arr.toList()
61
- .sortedBy { it.asJsonObject.get("name")?.asString ?: "" }
62
- .mapNotNull { el ->
63
- val p = el.asJsonObject
64
- val name = p.get("name")?.asString ?: return@mapNotNull null
65
- val path = p.get("path")?.asString ?: return@mapNotNull null
66
- val node = DefaultMutableTreeNode(TreeNodeData.LocalProject("πŸ“ $name", path, name))
67
-
68
- val bs = ForgeClient.get().request("/api/project-sessions?projectPath=${URLEncoder.encode(path, "UTF-8")}")
69
- val boundId = bs.data?.asJsonObject?.get("fixedSessionId")?.takeUnless { it.isJsonNull }?.asString
70
-
71
- val ss = ForgeClient.get().request("/api/claude-sessions/${URLEncoder.encode(name, "UTF-8")}")
72
- val sessions = ss.data?.takeIf { it.isJsonArray }?.asJsonArray?.toList() ?: emptyList()
73
-
74
- sessions.take(10).forEach { sEl ->
75
- val s = sEl.asJsonObject
76
- val sid = s.get("sessionId")?.asString ?: return@forEach
77
- val mtime = s.get("modified")?.asString ?: ""
78
- val isBound = sid == boundId
79
- val mark = if (isBound) "β˜…" else "Β·"
80
- val pretty = mtime.take(19).replace('T', ' ')
81
- node.add(DefaultMutableTreeNode(
82
- TreeNodeData.ClaudeSession("$mark ${sid.take(8)} $pretty", path, name, sid, isBound),
83
- ))
84
- }
85
- node.add(DefaultMutableTreeNode(TreeNodeData.NewSession("βž• New session…", path, name)))
86
- node
87
- }
88
- }
89
-
90
- override fun onDoubleClick(data: TreeNodeData, node: DefaultMutableTreeNode) {
91
- when (data) {
92
- is TreeNodeData.ClaudeSession -> resumeClaudeSession(data)
93
- is TreeNodeData.NewSession -> promptAgentAndLaunch(data.projectPath, data.projectName, resumeSessionId = null)
94
- is TreeNodeData.LocalProject -> promptAgentAndLaunch(data.projectPath, data.projectName, resumeSessionId = null)
95
- else -> {}
96
- }
97
- }
98
-
99
- override fun contextActions(data: TreeNodeData, node: DefaultMutableTreeNode): List<AnAction> = when (data) {
100
- is TreeNodeData.LocalProject -> buildList {
101
- add(buildOpenWithGroup(data.projectPath, data.projectName, resumeSessionId = null, label = "Open With (fresh)"))
102
- add(act("New Session… (pick agent)", AllIcons.General.Add) { promptAgentAndLaunch(data.projectPath, data.projectName, resumeSessionId = null) })
103
- add(act("Plain Terminal Here", AllIcons.Debugger.Console) { openPlainTerminal(data.projectPath, data.projectName) })
104
- }
105
- is TreeNodeData.ClaudeSession -> buildList {
106
- add(act("Resume (claude)", AllIcons.Actions.Execute) { resumeClaudeSession(data) })
107
- add(buildOpenWithGroup(data.projectPath, data.projectName, resumeSessionId = data.sessionId, label = "Resume With…"))
108
- if (!data.isBound) add(act("Pin as Default Session", AllIcons.Nodes.Favorite) { bindSession(data) })
109
- }
110
- is TreeNodeData.NewSession -> listOf(
111
- buildOpenWithGroup(data.projectPath, data.projectName, resumeSessionId = null, label = "Open With (fresh)"),
112
- act("New Session… (pick agent)", AllIcons.General.Add) { promptAgentAndLaunch(data.projectPath, data.projectName, resumeSessionId = null) },
113
- )
114
- else -> emptyList()
115
- }
116
-
117
- /** Submenu listing each cached agent β€” clicking one launches with the given resume mode. */
118
- private fun buildOpenWithGroup(projectPath: String, projectName: String, resumeSessionId: String?, label: String): DefaultActionGroup {
119
- val group = DefaultActionGroup(label, true)
120
- group.isPopup = true
121
- val ags = agentsCache
122
- if (ags.isEmpty()) {
123
- group.add(act("(loading agents β€” try again)", null) {})
124
- } else {
125
- for ((id, name, type) in ags) {
126
- val item = if (type != null) "$name ($type)" else name
127
- group.add(act(item, null) { launchAgent(projectPath, projectName, id, name, resumeSessionId) })
128
- }
129
- }
130
- return group
131
- }
132
-
133
- /** Resume a specific claude session β€” picks the first claude-type agent automatically. */
134
- private fun resumeClaudeSession(s: TreeNodeData.ClaudeSession) {
135
- runBg(project, "Resuming ${s.sessionId.take(8)}") {
136
- val ar = ForgeClient.get().request("/api/agents")
137
- val agents = ar.data?.asJsonObject?.getAsJsonArray("agents")?.toList()?.map { it.asJsonObject } ?: emptyList()
138
- val claude = agents.firstOrNull {
139
- val t = it.get("type")?.asString ?: it.get("cliType")?.asString
140
- t == "claude-code"
141
- } ?: agents.firstOrNull { it.get("id")?.asString?.startsWith("claude") == true }
142
- if (claude == null) {
143
- ApplicationManager.getApplication().invokeLater {
144
- notify(project, "Forge: no claude agent configured β€” open New Session… to pick another agent.", com.intellij.notification.NotificationType.WARNING)
145
- }
146
- return@runBg
147
- }
148
- launchAgent(s.projectPath, s.projectName,
149
- agentId = claude.get("id").asString,
150
- agentName = claude.get("name")?.asString ?: "claude",
151
- resumeSessionId = s.sessionId)
152
- }
153
- }
154
-
155
- /** Show an agent picker, then launch with the given resume mode. */
156
- private fun promptAgentAndLaunch(projectPath: String, projectName: String, resumeSessionId: String?) {
157
- runBg(project, "Loading agents") {
158
- val ar = ForgeClient.get().request("/api/agents")
159
- if (!ar.ok || ar.data == null || !ar.data.isJsonObject) {
160
- ApplicationManager.getApplication().invokeLater {
161
- notify(project, "Forge: failed to load agents β€” ${ar.error ?: "unknown"}", com.intellij.notification.NotificationType.ERROR)
162
- }
163
- return@runBg
164
- }
165
- val agents = ar.data.asJsonObject.getAsJsonArray("agents")?.toList()?.mapNotNull { el ->
166
- val o = el.asJsonObject
167
- if (o.get("enabled")?.asBoolean == false) return@mapNotNull null
168
- val id = o.get("id")?.asString ?: return@mapNotNull null
169
- val name = o.get("name")?.asString ?: id
170
- Triple(id, name, o.get("cliType")?.asString)
171
- }.orEmpty()
172
- if (agents.isEmpty()) {
173
- ApplicationManager.getApplication().invokeLater {
174
- notify(project, "Forge: no enabled agents β€” configure one in the Forge web UI.", com.intellij.notification.NotificationType.WARNING)
175
- }
176
- return@runBg
177
- }
178
- ApplicationManager.getApplication().invokeLater {
179
- val labels = agents.map { (_, name, type) -> if (type != null) "$name ($type)" else name }.toTypedArray()
180
- val choice = Messages.showEditableChooseDialog(
181
- "Choose an agent to launch in $projectName",
182
- if (resumeSessionId != null) "Forge: Resume With Agent" else "Forge: New Session",
183
- null, labels, labels[0], null,
184
- ) ?: return@invokeLater
185
- val idx = labels.indexOf(choice).let { if (it < 0) agents.indexOfFirst { (_, n, _) -> n == choice } else it }
186
- if (idx < 0) return@invokeLater
187
- val (agentId, agentName, _) = agents[idx]
188
- launchAgent(projectPath, projectName, agentId, agentName, resumeSessionId)
189
- }
190
- }
191
- }
192
-
193
- /** Resolve the agent's CLI command + env, build an argv, and spawn it directly
194
- * as the pty process β€” no intermediate shell, so the user sees the agent's UI
195
- * immediately with no leftover prompt fragments. */
196
- private fun launchAgent(projectPath: String, projectName: String, agentId: String, agentName: String, resumeSessionId: String?) {
197
- runBg(project, "Launching $agentName") {
198
- val rr = ForgeClient.get().request("/api/agents?resolve=${URLEncoder.encode(agentId, "UTF-8")}")
199
- val resolved = rr.data?.asJsonObject
200
- if (!rr.ok || resolved == null) {
201
- ApplicationManager.getApplication().invokeLater {
202
- notify(project, "Forge: cannot resolve $agentName β€” ${rr.error ?: "unknown"}", com.intellij.notification.NotificationType.ERROR)
203
- }
204
- return@runBg
205
- }
206
- val cliCmd = resolved.get("cliCmd")?.asString ?: agentId
207
- val supportsSession = resolved.get("supportsSession")?.asBoolean == true
208
- val cliType = resolved.get("cliType")?.asString
209
- // For claude, the API returns `-c` which is `--continue` (zero-arg) β€” passing a
210
- // session id after `-c` makes claude treat the id as initial prompt text and
211
- // resume the *most recent* session instead. Force `--resume` for specific-session
212
- // launches; this matches what forge's web UI does (see WebTerminal.tsx).
213
- val resumeFlag = if (cliType == "claude-code" && !resumeSessionId.isNullOrBlank()) {
214
- "--resume"
215
- } else {
216
- resolved.get("resumeFlag")?.asString ?: "--resume"
217
- }
218
- val envObj = resolved.getAsJsonObject("env")
219
- val envMap = mutableMapOf<String, String>()
220
- envObj?.entrySet()?.forEach { (k, v) -> if (!v.isJsonNull) envMap[k] = v.asString }
221
-
222
- // The resolve endpoint returns `model` separately from `env`. For claude this
223
- // becomes `--model <name>`; without it the agent silently falls back to claude's
224
- // default (opus), so a "sonnet" profile would appear to do nothing.
225
- val model = resolved.get("model")?.takeUnless { it.isJsonNull }?.asString
226
- val modelFlag = if (cliType == "claude-code" && !model.isNullOrBlank()) {
227
- " --model " + shellQuote(model)
228
- } else ""
229
-
230
- val cmdLine = buildString {
231
- append(cliCmd)
232
- if (supportsSession && !resumeSessionId.isNullOrBlank()) {
233
- append(' ').append(resumeFlag).append(' ').append(shellQuote(resumeSessionId))
234
- }
235
- append(modelFlag)
236
- }
237
- // Use the user's login shell so .zprofile/.bash_profile gets sourced β€” IDEA's
238
- // inherited PATH usually misses /opt/homebrew/bin, ~/.claude/local, etc. where
239
- // CLI agents actually live. `exec` makes the shell hand off the pty to the agent
240
- // so when the agent quits the tab closes; if exec fails (cmd not in PATH) we
241
- // fall through to a diagnostic so the error stays visible.
242
- val userShell = System.getenv("SHELL")?.takeIf { it.isNotBlank() } ?: "/bin/zsh"
243
- val script = """
244
- exec $cmdLine
245
- echo
246
- echo "[forge: failed to launch β€” '$cliCmd' not found in PATH]"
247
- echo "[forge: PATH=${'$'}PATH]"
248
- echo
249
- echo "(this terminal will close in 30s)"
250
- sleep 30
251
- """.trimIndent()
252
- val shellCmd = listOf(userShell, "-l", "-c", script)
253
-
254
- ApplicationManager.getApplication().invokeLater {
255
- spawnInTerminalTab(projectPath, "forge: $projectName ($agentName)", shellCmd, envMap)
256
- }
257
- }
258
- }
259
-
260
- /** Spawn `shellCmd` as the pty's primary process by intercepting
261
- * [LocalTerminalDirectRunner.configureStartupOptions]. */
262
- private fun spawnInTerminalTab(workingDir: String, tabName: String, shellCmd: List<String>, extraEnv: Map<String, String>) {
263
- val runner = object : LocalTerminalDirectRunner(project) {
264
- // Skip RC-file injection / command markers β€” our pty process isn't a shell.
265
- override fun enableShellIntegration(): Boolean = false
266
-
267
- override fun configureStartupOptions(baseOptions: ShellStartupOptions): ShellStartupOptions {
268
- val mergedEnv = HashMap<String, String>().apply {
269
- baseOptions.envVariables?.let { putAll(it) }
270
- putAll(extraEnv)
271
- putIfAbsent("TERM", "xterm-256color")
272
- }
273
- return baseOptions.builder()
274
- .shellCommand(shellCmd)
275
- .workingDirectory(baseOptions.workingDirectory ?: workingDir)
276
- .envVariables(mergedEnv)
277
- .build()
278
- }
279
-
280
- override fun getDefaultTabTitle(): String = tabName
281
- }
282
- val state = TerminalTabState().apply {
283
- myTabName = tabName
284
- myWorkingDirectory = workingDir
285
- }
286
- TerminalToolWindowManager.getInstance(project).createNewSession(runner, state)
287
- }
288
-
289
- private fun openPlainTerminal(projectPath: String, projectName: String) {
290
- TerminalToolWindowManager.getInstance(project)
291
- .createShellWidget(projectPath, "forge: $projectName", false, true)
292
- }
293
-
294
- private fun bindSession(s: TreeNodeData.ClaudeSession) {
295
- runApi(project, "Pin ${s.sessionId.take(8)}", {
296
- ForgeClient.get().request(
297
- "/api/project-sessions",
298
- method = "POST",
299
- body = mapOf("projectPath" to s.projectPath, "fixedSessionId" to s.sessionId),
300
- )
301
- }) { refresh() }
302
- }
303
-
304
- private fun shellQuote(s: String): String = "'" + s.replace("'", "'\\''") + "'"
305
-
306
- private fun act(name: String, icon: javax.swing.Icon?, run: () -> Unit) = object : AnAction(name, null, icon) {
307
- override fun actionPerformed(e: AnActionEvent) = run()
308
- }
309
- }
@@ -1,33 +0,0 @@
1
- package com.aion0.forge.ui.toolwindow
2
-
3
- /** Typed payloads attached to DefaultMutableTreeNode.userObject so click /
4
- * right-click handlers can branch on what was clicked. The `label` is what
5
- * ends up rendered (we just call .toString()). */
6
- sealed class TreeNodeData(val label: String) {
7
- override fun toString(): String = label
8
-
9
- // ── Workspaces tab ─────────────────────────────────────
10
- class Workspace(label: String, val workspaceId: String, val projectName: String, val daemonActive: Boolean) : TreeNodeData(label)
11
- class Smith(label: String, val workspaceId: String, val agentId: String, val agentLabel: String, val taskStatus: String, val paused: Boolean, val tmuxSession: String?) : TreeNodeData(label)
12
- // ── Terminals tab ──────────────────────────────────────
13
- /** A forge project (from /api/projects). Expand β†’ list of sessions; right-click β†’ new terminal. */
14
- class LocalProject(label: String, val projectPath: String, val projectName: String) : TreeNodeData(label)
15
- /** A claude session belonging to a project. Double-click β†’ resume in IDE terminal. */
16
- class ClaudeSession(label: String, val projectPath: String, val projectName: String, val sessionId: String, val isBound: Boolean) : TreeNodeData(label)
17
- /** "+ New session…" leaf shown under each project. */
18
- class NewSession(label: String, val projectPath: String, val projectName: String) : TreeNodeData(label)
19
-
20
- // ── Pipelines tab ──────────────────────────────────────
21
- class PipelineProject(label: String, val projectPath: String, val projectName: String) : TreeNodeData(label)
22
- class PipelineBinding(label: String, val projectPath: String, val projectName: String, val workflowName: String, val enabled: Boolean) : TreeNodeData(label)
23
- class PipelineRun(label: String, val pipelineId: String, val workflowName: String, val status: String) : TreeNodeData(label)
24
- class PipelineNode(label: String, val pipelineId: String, val nodeName: String, val status: String, val error: String?, val taskId: String?) : TreeNodeData(label)
25
-
26
- // ── Docs tab ───────────────────────────────────────────
27
- class DocRoot(label: String, val rootIdx: Int, val rootPath: String, val rootName: String) : TreeNodeData(label)
28
- class DocDir(label: String, val rootIdx: Int, val rootPath: String, val relPath: String) : TreeNodeData(label)
29
- class DocFile(label: String, val rootIdx: Int, val rootPath: String, val relPath: String, val fileType: String?) : TreeNodeData(label)
30
-
31
- // ── Misc ───────────────────────────────────────────────
32
- class Hint(label: String) : TreeNodeData(label)
33
- }