@aion0/forge 0.5.42 → 0.5.44

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 (70) hide show
  1. package/RELEASE_NOTES.md +16 -4
  2. package/components/Dashboard.tsx +22 -1
  3. package/components/WorkspaceView.tsx +15 -2
  4. package/intellij-plugin/README.md +53 -0
  5. package/intellij-plugin/build.gradle.kts +60 -0
  6. package/intellij-plugin/gradle/gradle-daemon-jvm.properties +12 -0
  7. package/intellij-plugin/gradle.properties +9 -0
  8. package/intellij-plugin/publish.sh +78 -0
  9. package/intellij-plugin/settings.gradle.kts +7 -0
  10. package/intellij-plugin/src/main/kotlin/com/aion0/forge/action/LoginAction.kt +49 -0
  11. package/intellij-plugin/src/main/kotlin/com/aion0/forge/action/LogoutAction.kt +18 -0
  12. package/intellij-plugin/src/main/kotlin/com/aion0/forge/action/OpenWebUIAction.kt +13 -0
  13. package/intellij-plugin/src/main/kotlin/com/aion0/forge/action/SwitchConnectionAction.kt +26 -0
  14. package/intellij-plugin/src/main/kotlin/com/aion0/forge/api/ForgeClient.kt +115 -0
  15. package/intellij-plugin/src/main/kotlin/com/aion0/forge/auth/Auth.kt +31 -0
  16. package/intellij-plugin/src/main/kotlin/com/aion0/forge/connection/ConnectionManager.kt +95 -0
  17. package/intellij-plugin/src/main/kotlin/com/aion0/forge/settings/ForgeConfigurable.kt +81 -0
  18. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/DocsView.kt +99 -0
  19. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/ForgeStatusBarWidgetFactory.kt +94 -0
  20. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/ForgeToolWindowFactory.kt +27 -0
  21. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/ForgeTreeView.kt +176 -0
  22. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/Helpers.kt +48 -0
  23. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/PipelinesView.kt +226 -0
  24. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/TerminalsView.kt +309 -0
  25. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/TreeNodeData.kt +33 -0
  26. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/WorkspacesView.kt +166 -0
  27. package/intellij-plugin/src/main/resources/META-INF/plugin.xml +88 -0
  28. package/intellij-plugin/src/main/resources/icons/forge.svg +3 -0
  29. package/lib/agents/index.ts +1 -1
  30. package/lib/help-docs/00-overview.md +1 -0
  31. package/lib/help-docs/11-workspace.md +2 -1
  32. package/lib/help-docs/13-ide-plugins.md +90 -0
  33. package/lib/help-docs/CLAUDE.md +3 -0
  34. package/lib/workspace/orchestrator.ts +80 -7
  35. package/lib/workspace/persistence.ts +16 -0
  36. package/lib/workspace/types.ts +13 -0
  37. package/lib/workspace/watch-manager.ts +65 -24
  38. package/lib/workspace-standalone.ts +5 -9
  39. package/next-env.d.ts +1 -1
  40. package/package.json +1 -1
  41. package/vscode-extension/.vscodeignore +11 -0
  42. package/vscode-extension/README.md +48 -0
  43. package/vscode-extension/media/icon.png +0 -0
  44. package/vscode-extension/media/icon.svg +3 -0
  45. package/vscode-extension/package-lock.json +4046 -0
  46. package/vscode-extension/package.json +514 -0
  47. package/vscode-extension/publish.sh +49 -0
  48. package/vscode-extension/src/api/client.ts +217 -0
  49. package/vscode-extension/src/auth/auth.ts +32 -0
  50. package/vscode-extension/src/commands/auth.ts +44 -0
  51. package/vscode-extension/src/commands/connection.ts +113 -0
  52. package/vscode-extension/src/commands/docs.ts +40 -0
  53. package/vscode-extension/src/commands/pipeline.ts +103 -0
  54. package/vscode-extension/src/commands/server.ts +50 -0
  55. package/vscode-extension/src/commands/smith.ts +112 -0
  56. package/vscode-extension/src/commands/task.ts +43 -0
  57. package/vscode-extension/src/commands/terminal.ts +279 -0
  58. package/vscode-extension/src/commands/workspace.ts +138 -0
  59. package/vscode-extension/src/connection/manager.ts +80 -0
  60. package/vscode-extension/src/docs/fs-provider.ts +94 -0
  61. package/vscode-extension/src/docs/result-provider.ts +33 -0
  62. package/vscode-extension/src/docs/transport.ts +22 -0
  63. package/vscode-extension/src/extension.ts +314 -0
  64. package/vscode-extension/src/statusbar.ts +70 -0
  65. package/vscode-extension/src/terminal/pseudoterm.ts +123 -0
  66. package/vscode-extension/src/views/docs.ts +145 -0
  67. package/vscode-extension/src/views/pipelines.ts +222 -0
  68. package/vscode-extension/src/views/terminals.ts +91 -0
  69. package/vscode-extension/src/views/workspaces.ts +243 -0
  70. package/vscode-extension/tsconfig.json +16 -0
@@ -0,0 +1,166 @@
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.project.Project
8
+ import com.intellij.openapi.ui.Messages
9
+ import org.jetbrains.plugins.terminal.ShellTerminalWidget
10
+ import org.jetbrains.plugins.terminal.TerminalView
11
+ import javax.swing.tree.DefaultMutableTreeNode
12
+
13
+ class WorkspacesView(project: Project) : ForgeTreeView(project) {
14
+
15
+ /** Cache of open smith terminals keyed by tmux session name. Re-clicking a smith
16
+ * focuses the existing terminal tab instead of spawning a duplicate. */
17
+ private val openedTerminals = mutableMapOf<String, ShellTerminalWidget>()
18
+
19
+ override fun rootLabel() = "workspaces"
20
+
21
+ override fun reload(): List<DefaultMutableTreeNode> {
22
+ val r = ForgeClient.get().request("/api/workspace")
23
+ if (r.status == 401 || r.status == 403) return listOf(DefaultMutableTreeNode(TreeNodeData.Hint("🔑 Tools → Forge: Login")))
24
+ if (!r.ok || r.data == null || !r.data.isJsonArray) {
25
+ return listOf(DefaultMutableTreeNode(TreeNodeData.Hint("⚠ ${r.error ?: "Not connected"}")))
26
+ }
27
+ val arr = r.data.asJsonArray
28
+ if (arr.size() == 0) return listOf(DefaultMutableTreeNode(TreeNodeData.Hint("No workspaces yet")))
29
+ val sorted = arr.toList().sortedBy { it.asJsonObject.get("projectName")?.asString ?: "" }
30
+ return sorted.mapNotNull { el ->
31
+ val ws = el.asJsonObject
32
+ val id = ws.get("id")?.asString ?: return@mapNotNull null
33
+ val name = ws.get("projectName")?.asString ?: id
34
+ val ar = ForgeClient.get().request("/api/workspace/$id/agents")
35
+ val agents = ar.data?.asJsonObject?.getAsJsonArray("agents")
36
+ val states = ar.data?.asJsonObject?.getAsJsonObject("states")
37
+ val daemon = ar.data?.asJsonObject?.get("daemonActive")?.asBoolean ?: false
38
+ val mark = if (daemon) "🟢" else "○"
39
+ val node = DefaultMutableTreeNode(TreeNodeData.Workspace("$mark $name (${agents?.size() ?: 0} smiths)", id, name, daemon))
40
+ agents?.forEach { aEl ->
41
+ val a = aEl.asJsonObject
42
+ val aId = a.get("id")?.asString ?: return@forEach
43
+ val label = a.get("label")?.asString ?: aId
44
+ val icon = a.get("icon")?.asString ?: "🤖"
45
+ val s = states?.getAsJsonObject(aId)
46
+ val task = s?.get("taskStatus")?.asString ?: "idle"
47
+ val smith = s?.get("smithStatus")?.asString ?: "down"
48
+ val paused = s?.get("paused")?.asBoolean == true
49
+ val tmux = s?.get("tmuxSession")?.asString
50
+ val statusEmoji = when {
51
+ paused -> "⏸"
52
+ smith == "down" -> "○"
53
+ smith == "starting"-> "◐"
54
+ task == "running" -> "▶"
55
+ task == "failed" -> "✕"
56
+ task == "done" -> "✓"
57
+ else -> "·"
58
+ }
59
+ node.add(DefaultMutableTreeNode(
60
+ TreeNodeData.Smith("$statusEmoji $icon $label ($task)", id, aId, label, task, paused, tmux),
61
+ ))
62
+ }
63
+ node
64
+ }
65
+ }
66
+
67
+ override fun onDoubleClick(data: TreeNodeData, node: DefaultMutableTreeNode) {
68
+ when (data) {
69
+ is TreeNodeData.Smith -> openSmithTerminal(data)
70
+ else -> {}
71
+ }
72
+ }
73
+
74
+ override fun contextActions(data: TreeNodeData, node: DefaultMutableTreeNode): List<AnAction> = when (data) {
75
+ is TreeNodeData.Workspace -> if (data.daemonActive) {
76
+ listOf(
77
+ act("Stop Daemon", AllIcons.Actions.Suspend) { confirmAndDoWs(data.workspaceId, "stop_daemon", "Stop the workspace daemon? Running smiths will be terminated.") },
78
+ act("Restart Daemon",AllIcons.Actions.Restart) { runApi(project, "Restart daemon", { restartDaemon(data.workspaceId) }) { refresh() } },
79
+ )
80
+ } else {
81
+ listOf(
82
+ act("Start Daemon", AllIcons.Actions.Execute) { runApi(project, "Start daemon", { wsAction(data.workspaceId, "start_daemon") }) { refresh() } },
83
+ )
84
+ }
85
+ is TreeNodeData.Smith -> buildList {
86
+ add(act("Open Terminal", AllIcons.Debugger.Console) { openSmithTerminal(data) })
87
+ add(act("Send Message", AllIcons.General.Balloon) { promptSendMessage(data) })
88
+ if (data.paused) {
89
+ add(act("Resume", AllIcons.Actions.Execute) { runApi(project, "Resume ${data.agentLabel}", { wsAction(data.workspaceId, "resume", mapOf("agentId" to data.agentId)) }) { refresh() } })
90
+ } else {
91
+ add(act("Pause", AllIcons.Actions.Suspend) { runApi(project, "Pause ${data.agentLabel}", { wsAction(data.workspaceId, "pause", mapOf("agentId" to data.agentId)) }) { refresh() } })
92
+ }
93
+ if (data.taskStatus == "running") {
94
+ add(act("Mark Done", null) { runApi(project, "Mark done", { wsAction(data.workspaceId, "mark_done", mapOf("agentId" to data.agentId, "notify" to true)) }) { refresh() } })
95
+ add(act("Mark Failed", null) { runApi(project, "Mark failed", { wsAction(data.workspaceId, "mark_failed", mapOf("agentId" to data.agentId, "notify" to true)) }) { refresh() } })
96
+ add(act("Mark Idle", null) { runApi(project, "Mark idle", { wsAction(data.workspaceId, "mark_done", mapOf("agentId" to data.agentId, "notify" to false)) }) { refresh() } })
97
+ }
98
+ if (data.taskStatus == "failed") {
99
+ add(act("Retry", AllIcons.Actions.Refresh) { runApi(project, "Retry ${data.agentLabel}", { wsAction(data.workspaceId, "retry", mapOf("agentId" to data.agentId)) }) { refresh() } })
100
+ }
101
+ }
102
+ else -> emptyList()
103
+ }
104
+
105
+ private fun confirmAndDoWs(wsId: String, action: String, prompt: String) {
106
+ val r = Messages.showYesNoDialog(project, prompt, "Forge", Messages.getQuestionIcon())
107
+ if (r == Messages.YES) runApi(project, action, { wsAction(wsId, action) }) { refresh() }
108
+ }
109
+
110
+ private fun restartDaemon(wsId: String): com.aion0.forge.api.ApiResult {
111
+ val stop = wsAction(wsId, "stop_daemon")
112
+ if (!stop.ok) return stop
113
+ Thread.sleep(800)
114
+ return wsAction(wsId, "start_daemon")
115
+ }
116
+
117
+ /** Open an IDE terminal and run `tmux attach -t <session>` — works for
118
+ * local forge. Remote forge would need a WebSocket-bridged JediTerm
119
+ * session (TODO). Reuses an existing terminal tab if one was already
120
+ * opened for this smith. */
121
+ private fun openSmithTerminal(smith: TreeNodeData.Smith) {
122
+ val res = ForgeClient.get().request(
123
+ "/api/workspace/${smith.workspaceId}/agents",
124
+ method = "POST",
125
+ body = mapOf("action" to "open_terminal", "agentId" to smith.agentId),
126
+ )
127
+ val tmux = res.data?.asJsonObject?.get("tmuxSession")?.asString ?: smith.tmuxSession
128
+ if (tmux.isNullOrBlank()) {
129
+ notify(project, "Forge: smith ${smith.agentLabel} has no tmux session yet — start the daemon first.", com.intellij.notification.NotificationType.WARNING)
130
+ return
131
+ }
132
+ val existing = openedTerminals[tmux]
133
+ if (existing != null && !com.intellij.openapi.util.Disposer.isDisposed(existing)) {
134
+ // Surface the existing tab in the Terminal tool window.
135
+ val tw = com.intellij.openapi.wm.ToolWindowManager.getInstance(project).getToolWindow("Terminal")
136
+ tw?.activate(null)
137
+ existing.requestFocusInWindow()
138
+ return
139
+ }
140
+ val terminalView = TerminalView.getInstance(project)
141
+ val widget = terminalView.createLocalShellWidget(project.basePath ?: System.getProperty("user.home"), "forge: ${smith.agentLabel}")
142
+ openedTerminals[tmux] = widget
143
+ com.intellij.openapi.util.Disposer.register(widget) { openedTerminals.remove(tmux) }
144
+ // Force UTF-8 + 256-color terminfo so the tmux UI doesn't render as garbled
145
+ // box-drawing characters in JediTerm. `-u` opts into UTF-8, `-2` forces 256-color.
146
+ widget.executeCommand("TERM=xterm-256color tmux -2 -u attach -t \"$tmux\" || TERM=xterm-256color tmux -2 -u new -A -s \"$tmux\"")
147
+ }
148
+
149
+ private fun promptSendMessage(smith: TreeNodeData.Smith) {
150
+ val text = Messages.showMultilineInputDialog(
151
+ project,
152
+ "Send message to ${smith.agentLabel}",
153
+ "Forge: Send Message",
154
+ "",
155
+ null, null,
156
+ ) ?: return
157
+ if (text.isBlank()) return
158
+ runApi(project, "Send message to ${smith.agentLabel}",
159
+ { wsAction(smith.workspaceId, "message", mapOf("agentId" to smith.agentId, "content" to text)) },
160
+ ) { refresh() }
161
+ }
162
+
163
+ private fun act(name: String, icon: javax.swing.Icon?, run: () -> Unit) = object : AnAction(name, null, icon) {
164
+ override fun actionPerformed(e: AnActionEvent) = run()
165
+ }
166
+ }
@@ -0,0 +1,88 @@
1
+ <idea-plugin>
2
+ <id>com.aion0.forge</id>
3
+ <name>Forge Vibe Coding</name>
4
+ <version>0.1.0</version>
5
+ <vendor email="liuzhen1984@gmail.com" url="https://github.com/aiwatching/forge">aion0</vendor>
6
+
7
+ <description><![CDATA[
8
+ <p>Drive your <a href="https://github.com/aiwatching/forge">Forge</a> instance from
9
+ the IDE — workspaces, agents, terminals, pipelines and docs in one tool window.</p>
10
+
11
+ <p><b>⚠ Requires Forge running locally.</b> Install once:</p>
12
+ <pre>npm install -g @aion0/forge
13
+ forge server start</pre>
14
+ <p>Default port <code>8403</code>. Configure remote forges under
15
+ <b>Settings → Tools → Forge</b>.</p>
16
+
17
+ <h3>Features</h3>
18
+ <ul>
19
+ <li>Workspaces — daemon control, multi-agent smiths, tmux attach, pause/resume/retry</li>
20
+ <li>Terminals — per-project claude sessions, double-click to resume, right-click → Open With for any agent</li>
21
+ <li>Pipelines — bindings + run details with per-node prompt / result / diff viewer</li>
22
+ <li>Docs — open files in editor, "open terminal here"</li>
23
+ <li>Multi-connection — local + remote forges with status-bar switcher</li>
24
+ </ul>
25
+ ]]></description>
26
+
27
+ <change-notes><![CDATA[
28
+ <h3>0.1.0</h3>
29
+ <ul>
30
+ <li>Initial scaffold: tool window shell, settings page, login flow, multi-connection support.</li>
31
+ </ul>
32
+ ]]></change-notes>
33
+
34
+ <depends>com.intellij.modules.platform</depends>
35
+ <depends>org.jetbrains.plugins.terminal</depends>
36
+
37
+ <extensions defaultExtensionNs="com.intellij">
38
+ <!-- Persisted application-level state (connections + active connection). -->
39
+ <applicationService serviceImplementation="com.aion0.forge.connection.ConnectionManager"/>
40
+ <applicationService serviceImplementation="com.aion0.forge.api.ForgeClient"/>
41
+ <applicationService serviceImplementation="com.aion0.forge.auth.Auth"/>
42
+
43
+ <!-- Tool window: lives on the right side, contains tabs for each section. -->
44
+ <toolWindow
45
+ id="Forge"
46
+ anchor="left"
47
+ secondary="true"
48
+ icon="/icons/forge.svg"
49
+ factoryClass="com.aion0.forge.ui.toolwindow.ForgeToolWindowFactory"/>
50
+
51
+ <!-- Settings page (Preferences → Tools → Forge). -->
52
+ <applicationConfigurable
53
+ parentId="tools"
54
+ instance="com.aion0.forge.settings.ForgeConfigurable"
55
+ id="com.aion0.forge.settings.ForgeConfigurable"
56
+ displayName="Forge"/>
57
+
58
+ <!-- Status bar widget — connection state + click-to-switch. -->
59
+ <statusBarWidgetFactory
60
+ id="com.aion0.forge.statusbar"
61
+ implementation="com.aion0.forge.ui.toolwindow.ForgeStatusBarWidgetFactory"
62
+ order="last"/>
63
+
64
+ <notificationGroup id="Forge" displayType="BALLOON"/>
65
+ </extensions>
66
+
67
+ <actions>
68
+ <group id="com.aion0.forge.actions" text="Forge" popup="true">
69
+ <action id="com.aion0.forge.action.LoginAction"
70
+ class="com.aion0.forge.action.LoginAction"
71
+ text="Forge: Login"
72
+ description="Authenticate against the active Forge server."/>
73
+ <action id="com.aion0.forge.action.LogoutAction"
74
+ class="com.aion0.forge.action.LogoutAction"
75
+ text="Forge: Logout"
76
+ description="Clear the saved token for the active connection."/>
77
+ <action id="com.aion0.forge.action.SwitchConnectionAction"
78
+ class="com.aion0.forge.action.SwitchConnectionAction"
79
+ text="Forge: Switch Connection…"
80
+ description="Pick a different Forge server."/>
81
+ <action id="com.aion0.forge.action.OpenWebUIAction"
82
+ class="com.aion0.forge.action.OpenWebUIAction"
83
+ text="Forge: Open Web UI"
84
+ description="Open the Forge web UI in the default browser."/>
85
+ <add-to-group group-id="ToolsMenu" anchor="last"/>
86
+ </group>
87
+ </actions>
88
+ </idea-plugin>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="currentColor">
2
+ <path d="M12 2L3 7v6c0 5 3.5 9.5 9 11 5.5-1.5 9-6 9-11V7l-9-5zm0 4.5l5 2.8v3.4l-5-2.8-5 2.8V9.3l5-2.8zm-5 6.4l5 2.8v5.6c-3.4-1.4-5-4.6-5-7.6v-.8zm10 .8c0 3-1.6 6.2-5 7.6v-5.6l5-2.8v.8z"/>
3
+ </svg>
@@ -209,7 +209,7 @@ export function resolveTerminalLaunch(agentId?: string): TerminalLaunchInfo {
209
209
 
210
210
  // Determine CLI command and capabilities from cliType
211
211
  const cliMap: Record<string, { cmd: string; session: boolean; resume: string }> = {
212
- 'claude-code': { cmd: 'claude', session: true, resume: '-c' },
212
+ 'claude-code': { cmd: 'claude', session: true, resume: '--resume' },
213
213
  'codex': { cmd: 'codex', session: false, resume: '' },
214
214
  'aider': { cmd: 'aider', session: false, resume: '' },
215
215
  'generic': { cmd: agentCfg.path || agentId || 'claude', session: false, resume: '' },
@@ -17,6 +17,7 @@ Forge is a self-hosted Vibe Coding platform for Claude Code. It provides a brows
17
17
  | **Remote Access** | One-click Cloudflare tunnel for remote browsing |
18
18
  | **GitHub Issue Auto-fix** | Scan issues, auto-fix, create PRs |
19
19
  | **Memory (optional)** | Code graph + knowledge via `@aion0/temper` MCP server |
20
+ | **IDE Plugins** | First-party VSCode extension and IntelliJ plugin — drive workspaces, agents, terminals, pipelines and docs from inside the editor (see `13-ide-plugins.md`) |
20
21
 
21
22
  ## Quick Start
22
23
 
@@ -414,7 +414,7 @@ Each smith can display an animated companion character next to its node.
414
414
  | **Stop Daemon** | Stop all smiths, kill workers. Preserves user's terminal conversation context (no `/clear` is sent). Tmux sessions attached to by a user are kept alive. |
415
415
  | **Run All** | Trigger all runnable agents once |
416
416
  | **Run** | Trigger specific agent |
417
- | **Pause/Resume** | Pause/resume message consumption for one agent |
417
+ | **Pause/Resume** | Pause stops new bus pickups, drops queued + incoming messages to `failed`, and suppresses watch-alert dispatch. In-flight task continues. Resume re-enables. The pause flag is transient — daemon stop/start or process restart clears it. UI: ⏸ / ▶ icon on the smith card. |
418
418
  | **Mark Done/Failed/Idle** | Manually set task status |
419
419
  | **Retry** | Re-run a failed agent from checkpoint |
420
420
  | **Open Terminal** | Enter manual mode with tmux session |
@@ -592,6 +592,7 @@ Use this exact JSON structure when calling `POST /api/workspace/<id>/agents` wit
592
592
 
593
593
  - `action` values: `log` | `analyze` | `approve` | `send_message`
594
594
  - `sendTo` is required only when `action: "send_message"`
595
+ - `agent_status` target is event-driven — orchestrator stamps a transition timestamp on every real `taskStatus` change (idempotent re-emits are deduped). The watch tick compares timestamps and only fires when the target has settled to a non-busy state (not `running`/`starting`) and matches `pattern`. Does not depend on polling, so sub-tick task transitions are caught reliably. Pattern matches the **final** state only — intermediate states during the interval window don't affect matching.
595
596
 
596
597
  ## Complete Recipes
597
598
 
@@ -0,0 +1,90 @@
1
+ # IDE Plugins
2
+
3
+ Forge ships with two first-party IDE plugins that mirror the web UI's feature set so you can drive workspaces, agent terminals, pipelines and docs without leaving the editor. Both are **thin clients** — they require a Forge server running locally (or reachable over a tunnel).
4
+
5
+ | | VSCode extension | IntelliJ plugin |
6
+ |---|---|---|
7
+ | Marketplace ID | `aion0.forge-vibecoding` | `Forge Vibe Coding` (`com.aion0.forge`) |
8
+ | Install | VSCode → Extensions → search `Forge Vibe Coding` | IDE → Settings → Plugins → Marketplace → search `Forge Vibe Coding` |
9
+ | Source | `vscode-extension/` | `intellij-plugin/` |
10
+ | Min IDE | VSCode ≥ 1.80 | IntelliJ Platform ≥ 2024.1 (build 241) |
11
+
12
+ ## Prerequisites
13
+
14
+ The plugins do **not** ship Forge — install it once first:
15
+
16
+ ```bash
17
+ npm install -g @aion0/forge
18
+ forge server start
19
+ ```
20
+
21
+ Default port `8403`. The plugins auto-detect `http://localhost:8403`. For remote forges (over Cloudflare tunnel or LAN), add a connection in plugin settings (see below).
22
+
23
+ ## Tool Window / Sidebar Layout
24
+
25
+ Both plugins expose four tabs:
26
+
27
+ | Tab | Shows | Right-click actions | Double-click |
28
+ |---|---|---|---|
29
+ | **Workspaces** | Forge workspaces with daemon status (🟢/○) and per-smith status emoji (▶ running / ⏸ paused / ✓ done / ✕ failed / ◐ starting) | Workspace: start / stop / restart daemon. Smith: open terminal, send message, pause/resume, mark done/failed/idle, retry. | Smith → attach to its tmux session in an IDE terminal |
30
+ | **Terminals** | Each forge project as a folder with its claude sessions (★ = bound/pinned default). | Project: **Open With ▸** submenu of every configured agent (claude/codex/aider/...) — fresh launch; New Session… (pick agent); Plain Terminal Here. Session: Resume; Resume With… submenu; Pin as Default Session. | Session row resumes that exact session via `claude --resume <id>` |
31
+ | **Pipelines** | Forge projects with their pipeline bindings (⚙ enabled / ⊘ disabled) and recent runs (▶/✓/✕/⊘). | Project: Add Pipeline…; Binding: Trigger Now, Enable/Disable, Remove; Run: Show Nodes; Node: Show Result. | Binding: trigger; Run: expand to see nodes; Node: open prompt/result/diff/log as a markdown buffer |
32
+ | **Docs** | Configured doc roots → file/dir tree. | Dir: Open Terminal Here (runs claude); File: Open. | File: open in IDE editor |
33
+
34
+ ## Multi-Connection (Local + Remote forges)
35
+
36
+ Both plugins support multiple Forge servers — useful when you have one Forge running locally for `~/.forge` and a second running on an office Mac mini exposed via tunnel.
37
+
38
+ **VSCode**: edit `forge.connections` in settings.json:
39
+ ```json
40
+ {
41
+ "forge.connections": [
42
+ { "name": "Local", "serverUrl": "http://localhost:8403", "terminalUrl": "ws://localhost:8404" },
43
+ { "name": "Office", "serverUrl": "https://forge-office.trycloudflare.com", "terminalUrl": "wss://forge-office.trycloudflare.com" }
44
+ ],
45
+ "forge.activeConnection": "Local"
46
+ }
47
+ ```
48
+ Click the status-bar entry (bottom right, `Forge: <name>`) to switch.
49
+
50
+ **IntelliJ**: Settings → Tools → Forge → Connections list. Status-bar widget toggles active connection.
51
+
52
+ Tokens are stored per-connection: VSCode uses `SecretStorage`, IntelliJ uses `PasswordSafe`. `Forge: Login` (command palette / Tools menu) prompts for the admin password and caches the bearer token.
53
+
54
+ ## How agent terminals work
55
+
56
+ When you double-click a session row or pick `Open With ▸ <agent>`:
57
+
58
+ 1. Plugin calls `GET /api/agents?resolve=<agentId>` → gets `cliCmd`, `cliType`, `supportsSession`, `env`, `model`.
59
+ 2. For specific-session resume: `claude --resume <sid>` (forced regardless of API's `resumeFlag` — `-c` is wrong for specific resume).
60
+ 3. Profile env vars are forwarded to the spawned process.
61
+ 4. `model` is passed as `--model <name>` for claude-code agents (so a "sonnet" profile actually runs sonnet).
62
+ 5. **IntelliJ** spawns the agent CLI directly as the pty's primary process (`LocalTerminalDirectRunner` subclass with `enableShellIntegration = false` + `configureStartupOptions` override) — no shell, no `executeCommand` race. The user's login shell (`$SHELL -l`) is wrapped around it so `.zprofile` / `.bash_profile` is sourced for PATH.
63
+ 6. **VSCode** uses the existing terminal-server WebSocket (`forge.terminalUrl`) to attach.
64
+
65
+ Workspace smith terminals attach to a pre-existing tmux session via `tmux -2 -u attach -t <session>` (UTF-8 + 256-color forced, otherwise JediTerm/xterm.js render boxes wrong).
66
+
67
+ ## Releasing new versions (maintainers)
68
+
69
+ Each plugin has a `publish.sh` in its directory:
70
+
71
+ ```bash
72
+ # VSCode — needs `vsce login aion0` cached, or $VSCE_PAT env var
73
+ cd vscode-extension
74
+ ./publish.sh patch # bump 0.2.x → 0.2.(x+1) and publish
75
+
76
+ # IntelliJ — needs $JETBRAINS_MARKETPLACE_TOKEN env var
77
+ cd intellij-plugin
78
+ ./publish.sh patch
79
+ ```
80
+
81
+ VSCode goes live in 1–2 minutes. IntelliJ goes through human moderation: 1–3 business days for the first publish, hours-to-1-day for subsequent updates of an already-approved plugin.
82
+
83
+ ## Troubleshooting
84
+
85
+ - **"Not connected" / "401" in tree** — Forge server isn't running or the token expired. Run `Forge: Login` (or `forge server start` if the server is down).
86
+ - **Terminal opens but agent CLI fails** ("`claude: command not found`") — IDE inherited a stripped PATH. The IntelliJ plugin spawns under `$SHELL -l -c …` to source `.zprofile`/`.bash_profile`; if your PATH is set in `.zshrc` only (not `.zprofile`), move it.
87
+ - **Smith terminal shows garbled output** — make sure tmux ≥ 3.0 and the IDE terminal supports UTF-8 + 256-color. The plugin already passes `tmux -2 -u attach`; if it still looks broken, check `$LANG` is `*.UTF-8`.
88
+ - **Picked a session, claude opens a different one** — Forge ≤ 0.5.43 had `resume: '-c'` hardcoded for claude (which is `--continue`, zero-arg). Upgrade Forge or pull the latest `lib/agents/index.ts`.
89
+ - **JetBrains plugin install fails with "incompatible build"** — verify your IDE is build 241+ (Help → About → Build #). The plugin's `sinceBuild = 241`.
90
+ - **Tree keeps collapsing folders I expanded** — fixed in IntelliJ plugin v0.1.17 (full path expansion is preserved across the 5-second poll, not just top-level).
@@ -43,6 +43,7 @@ The token is valid for 24 hours. Store it in a variable and reuse for all API ca
43
43
  | `10-troubleshooting.md` | Common issues and solutions |
44
44
  | `11-workspace.md` | Workspace (Forge Smiths) — multi-agent orchestration, daemon, message bus, profiles |
45
45
  | `12-usage.md` | Token usage analytics — charts, heatmap, cost estimation, by model/project/source |
46
+ | `13-ide-plugins.md` | VSCode extension + IntelliJ plugin — install, tabs, multi-connection, agent terminal launching |
46
47
 
47
48
  ## Matching questions to docs
48
49
 
@@ -64,3 +65,5 @@ The token is valid for 24 hours. Store it in a variable and reuse for all API ca
64
65
  - Usage/cost/tokens/spending/billing/analytics → `12-usage.md`
65
66
  - Terminal dock/float/mouse toggle/reconnect → `07-projects.md` + `11-workspace.md`
66
67
  - Sidebar collapse/project tabs/favorites → `07-projects.md`
68
+ - VSCode/IntelliJ/IDE plugin/extension/marketplace → `13-ide-plugins.md`
69
+ - vsce/vsix/JetBrains marketplace publish → `13-ide-plugins.md`
@@ -39,7 +39,7 @@ import {
39
39
  loadMemory, saveMemory, createMemory, formatMemoryForPrompt,
40
40
  addObservation, addSessionSummary, parseStepToObservations, buildSessionSummary,
41
41
  } from './smith-memory';
42
- import { getFixedSession } from '../project-sessions';
42
+ import { getFixedSession, setFixedSession } from '../project-sessions';
43
43
 
44
44
  // ─── Workspace Topology Cache ────────────────────────────
45
45
 
@@ -116,12 +116,29 @@ export class WorkspaceOrchestrator extends EventEmitter {
116
116
  if (event.type === 'log' && event.agentId && event.entry) {
117
117
  appendAgentLog(this.workspaceId, event.agentId, event.entry).catch(() => {});
118
118
  }
119
+ // Stamp taskStatus transitions. Idempotent re-emits of the same value
120
+ // don't bump the timestamp — agent_status watches rely on this to detect
121
+ // real transitions without polling.
122
+ if (event.type === 'task_status' && event.agentId && event.taskStatus) {
123
+ const entry = this.agents.get(event.agentId);
124
+ if (entry && entry.state.lastTaskStatus !== event.taskStatus) {
125
+ const prev = entry.state.lastTaskStatus;
126
+ entry.state.lastTaskStatus = event.taskStatus;
127
+ entry.state.taskStatusChangedAt = Date.now();
128
+ console.log(`[task_status] ${entry.config.label}: ${prev || '(none)'} → ${event.taskStatus} @ ${entry.state.taskStatusChangedAt}`);
129
+ }
130
+ }
119
131
  });
120
132
  // Handle watch events
121
133
  this.watchManager.on('watch_alert', (event) => {
134
+ const alertEntry = this.agents.get(event.agentId);
135
+ // Paused source smith — observation continues but no dispatch.
136
+ if (alertEntry?.state.paused) {
137
+ console.log(`[watch] ${alertEntry.config.label}: paused — alert dropped`);
138
+ return;
139
+ }
122
140
  this.emit('event', event);
123
141
  // Push alert to agent history so Log panel shows it
124
- const alertEntry = this.agents.get(event.agentId);
125
142
  if (alertEntry && event.entry) {
126
143
  alertEntry.state.history.push(event.entry);
127
144
  this.emit('event', { type: 'log', agentId: event.agentId, entry: event.entry } as any);
@@ -266,6 +283,21 @@ export class WorkspaceOrchestrator extends EventEmitter {
266
283
  return null;
267
284
  }
268
285
 
286
+ /** Resolve the primary smith's fixed session for this project. If none is bound
287
+ * yet, auto-bind to the latest existing claude session so the terminal picker
288
+ * can offer "Current Session" instead of forcing the user into a fresh shell. */
289
+ resolvePrimaryFixedSession(): string | null {
290
+ const existing = getFixedSession(this.projectPath);
291
+ if (existing) return existing;
292
+ const primary = this.getPrimaryAgent();
293
+ if (!primary) return null;
294
+ const latest = this.getLatestSessionId(primary.config.workDir);
295
+ if (!latest) return null;
296
+ setFixedSession(this.projectPath, latest);
297
+ console.log(`[workspace] primary auto-bound to existing session ${latest} for ${this.projectPath}`);
298
+ return latest;
299
+ }
300
+
269
301
  addAgent(config: WorkspaceAgentConfig): void {
270
302
  const conflict = this.validateOutputs(config);
271
303
  if (conflict) throw new Error(conflict);
@@ -440,7 +472,7 @@ export class WorkspaceOrchestrator extends EventEmitter {
440
472
  const workerState = entry.worker?.getState();
441
473
  // Merge: worker state for task/smith, entry.state for mode (orchestrator controls mode)
442
474
  result[id] = workerState
443
- ? { ...workerState, taskStatus: entry.state.taskStatus, tmuxSession: entry.state.tmuxSession, currentMessageId: entry.state.currentMessageId }
475
+ ? { ...workerState, taskStatus: entry.state.taskStatus, tmuxSession: entry.state.tmuxSession, currentMessageId: entry.state.currentMessageId, paused: entry.state.paused }
444
476
  : entry.state;
445
477
  }
446
478
  return result;
@@ -930,6 +962,9 @@ export class WorkspaceOrchestrator extends EventEmitter {
930
962
  // Clean up stale state from previous run
931
963
  this.bus.markAllRunningAsFailed();
932
964
 
965
+ // Paused is transient — clear on every daemon start so any leftover flag goes away.
966
+ for (const entry of this.agents.values()) entry.state.paused = false;
967
+
933
968
  // Install forge skills globally (once per daemon start)
934
969
  try {
935
970
  installForgeSkills(this.projectPath, this.workspaceId, '', Number(process.env.PORT) || 8403);
@@ -1167,6 +1202,9 @@ export class WorkspaceOrchestrator extends EventEmitter {
1167
1202
  for (const [id, entry] of this.agents) {
1168
1203
  if (entry.config.type === 'input') continue;
1169
1204
 
1205
+ // 0. Clear transient paused flag — daemon stop should always reset it.
1206
+ entry.state.paused = false;
1207
+
1170
1208
  // 1. Stop message loop
1171
1209
  this.stopMessageLoop(id);
1172
1210
 
@@ -1633,16 +1671,40 @@ export class WorkspaceOrchestrator extends EventEmitter {
1633
1671
  return this.daemonActive;
1634
1672
  }
1635
1673
 
1636
- /** Pause a running agent */
1674
+ /** Pause a smith drop pending/incoming bus messages as failed, suppress watch alerts.
1675
+ * In-flight task continues. Resume does NOT replay dropped messages.
1676
+ * Transient: any daemon stop/start or process restart clears the flag. */
1637
1677
  pauseAgent(agentId: string): void {
1638
1678
  const entry = this.agents.get(agentId);
1639
- entry?.worker?.pause();
1679
+ if (!entry) return;
1680
+ entry.state.paused = true;
1681
+ entry.worker?.pause();
1682
+
1683
+ // Drain inbox: anything queued for this smith → failed (visible in inbox).
1684
+ let drained = 0;
1685
+ for (const m of this.bus.getLog()) {
1686
+ if (m.to !== agentId || m.type === 'ack') continue;
1687
+ if (m.status === 'pending' || m.status === 'pending_approval') {
1688
+ m.status = 'failed' as any;
1689
+ drained++;
1690
+ this.emit('event', { type: 'bus_message_status', messageId: m.id, status: 'failed' } as any);
1691
+ }
1692
+ }
1693
+
1694
+ this.emit('event', { type: 'task_status', agentId, taskStatus: entry.state.taskStatus } as any);
1695
+ this.emitAgentsChanged();
1696
+ console.log(`[workspace] ${entry.config.label}: paused${drained ? ` (${drained} pending message(s) dropped to failed)` : ''}`);
1640
1697
  }
1641
1698
 
1642
- /** Resume a paused agent */
1699
+ /** Resume a paused smith — clear flag, re-enable bus pickup and watch dispatch. */
1643
1700
  resumeAgent(agentId: string): void {
1644
1701
  const entry = this.agents.get(agentId);
1645
- entry?.worker?.resume();
1702
+ if (!entry) return;
1703
+ entry.state.paused = false;
1704
+ entry.worker?.resume();
1705
+ this.emit('event', { type: 'task_status', agentId, taskStatus: entry.state.taskStatus } as any);
1706
+ this.emitAgentsChanged();
1707
+ console.log(`[workspace] ${entry.config.label}: resumed`);
1646
1708
  }
1647
1709
 
1648
1710
  /** Stop a running agent */
@@ -2902,6 +2964,14 @@ Silently ingest this context. Do NOT respond — await an actual task.`;
2902
2964
  // ── Store message in agent history ──
2903
2965
  target.state.history.push(logEntry);
2904
2966
 
2967
+ // ── Paused smith → drop as failed; user retries/deletes from inbox. ──
2968
+ if (target.state.paused) {
2969
+ msg.status = 'failed' as any;
2970
+ this.emit('event', { type: 'bus_message_status', messageId: msg.id, status: 'failed' } as any);
2971
+ console.log(`[bus] ${target.config.label}: paused — ${action} dropped to failed`);
2972
+ return;
2973
+ }
2974
+
2905
2975
  // ── requiresApproval → set pending_approval on arrival ──
2906
2976
  if (target.config.requiresApproval) {
2907
2977
  msg.status = 'pending_approval';
@@ -2933,6 +3003,9 @@ Silently ingest this context. Do NOT respond — await an actual task.`;
2933
3003
  // (loop stays alive so it works when smith comes back)
2934
3004
  if (entry.state.smithStatus !== 'active') return;
2935
3005
 
3006
+ // Paused smiths refuse new bus pickups; loop stays alive for resume.
3007
+ if (entry.state.paused) return;
3008
+
2936
3009
  // Skip if already busy
2937
3010
  if (entry.state.taskStatus === 'running') return;
2938
3011
 
@@ -53,6 +53,9 @@ export async function saveWorkspace(state: WorkspaceState): Promise<void> {
53
53
  ...s,
54
54
  history: [],
55
55
  logFile: agentLogFile(state.id, id),
56
+ paused: undefined, // transient — never persisted
57
+ taskStatusChangedAt: undefined, // transient
58
+ lastTaskStatus: undefined, // transient
56
59
  }])
57
60
  ),
58
61
  updatedAt: Date.now(),
@@ -90,6 +93,9 @@ export function saveWorkspaceSync(state: WorkspaceState): void {
90
93
  ...s,
91
94
  history: [],
92
95
  logFile: agentLogFile(state.id, id),
96
+ paused: undefined, // transient — never persisted
97
+ taskStatusChangedAt: undefined, // transient
98
+ lastTaskStatus: undefined, // transient
93
99
  }])
94
100
  ),
95
101
  updatedAt: Date.now(),
@@ -160,6 +166,16 @@ export function loadWorkspace(workspaceId: string): WorkspaceState | null {
160
166
  if (agentState.taskStatus === 'running') {
161
167
  agentState.taskStatus = 'idle';
162
168
  }
169
+
170
+ // Defensive: paused is transient. Force false on load in case any
171
+ // older state.json still has it persisted.
172
+ agentState.paused = false;
173
+
174
+ // Init the agent_status watch fields. Setting changedAt to now() means
175
+ // any transitions that happen after load advance the timestamp; watchers
176
+ // record this baseline at their first tick and won't spuriously fire.
177
+ agentState.taskStatusChangedAt = Date.now();
178
+ agentState.lastTaskStatus = agentState.taskStatus;
163
179
  }
164
180
 
165
181
  // Migrate Input nodes: content → entries
@@ -99,6 +99,19 @@ export interface AgentState {
99
99
  // ─── Task layer (current work) ──────────
100
100
  taskStatus: TaskStatus; // idle/running/done/failed
101
101
 
102
+ // Transient runtime flag — paused smith ignores bus pickups, drops new
103
+ // incoming messages as failed, and suppresses watch alerts. Never persisted;
104
+ // any daemon stop/start or process restart clears it.
105
+ paused?: boolean;
106
+
107
+ // Transient: timestamp of the most recent taskStatus *change* (idempotent
108
+ // re-emits of the same value don't update this). Used by agent_status watches
109
+ // to detect transitions without polling. Reset to Date.now() on load.
110
+ taskStatusChangedAt?: number;
111
+ // Transient: previous taskStatus value, used by orchestrator's event listener
112
+ // to dedup idempotent emits. Reset to current taskStatus on load.
113
+ lastTaskStatus?: TaskStatus;
114
+
102
115
  // ─── Execution details ──────────────────
103
116
  currentStep?: number;
104
117
  history: TaskLogEntry[];