@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.
- package/RELEASE_NOTES.md +16 -4
- package/components/Dashboard.tsx +22 -1
- package/components/WorkspaceView.tsx +15 -2
- package/intellij-plugin/README.md +53 -0
- package/intellij-plugin/build.gradle.kts +60 -0
- package/intellij-plugin/gradle/gradle-daemon-jvm.properties +12 -0
- package/intellij-plugin/gradle.properties +9 -0
- package/intellij-plugin/publish.sh +78 -0
- package/intellij-plugin/settings.gradle.kts +7 -0
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/action/LoginAction.kt +49 -0
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/action/LogoutAction.kt +18 -0
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/action/OpenWebUIAction.kt +13 -0
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/action/SwitchConnectionAction.kt +26 -0
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/api/ForgeClient.kt +115 -0
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/auth/Auth.kt +31 -0
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/connection/ConnectionManager.kt +95 -0
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/settings/ForgeConfigurable.kt +81 -0
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/DocsView.kt +99 -0
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/ForgeStatusBarWidgetFactory.kt +94 -0
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/ForgeToolWindowFactory.kt +27 -0
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/ForgeTreeView.kt +176 -0
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/Helpers.kt +48 -0
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/PipelinesView.kt +226 -0
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/TerminalsView.kt +309 -0
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/TreeNodeData.kt +33 -0
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/WorkspacesView.kt +166 -0
- package/intellij-plugin/src/main/resources/META-INF/plugin.xml +88 -0
- package/intellij-plugin/src/main/resources/icons/forge.svg +3 -0
- package/lib/agents/index.ts +1 -1
- package/lib/help-docs/00-overview.md +1 -0
- package/lib/help-docs/11-workspace.md +2 -1
- package/lib/help-docs/13-ide-plugins.md +90 -0
- package/lib/help-docs/CLAUDE.md +3 -0
- package/lib/workspace/orchestrator.ts +80 -7
- package/lib/workspace/persistence.ts +16 -0
- package/lib/workspace/types.ts +13 -0
- package/lib/workspace/watch-manager.ts +65 -24
- package/lib/workspace-standalone.ts +5 -9
- package/next-env.d.ts +1 -1
- package/package.json +1 -1
- package/vscode-extension/.vscodeignore +11 -0
- package/vscode-extension/README.md +48 -0
- package/vscode-extension/media/icon.png +0 -0
- package/vscode-extension/media/icon.svg +3 -0
- package/vscode-extension/package-lock.json +4046 -0
- package/vscode-extension/package.json +514 -0
- package/vscode-extension/publish.sh +49 -0
- package/vscode-extension/src/api/client.ts +217 -0
- package/vscode-extension/src/auth/auth.ts +32 -0
- package/vscode-extension/src/commands/auth.ts +44 -0
- package/vscode-extension/src/commands/connection.ts +113 -0
- package/vscode-extension/src/commands/docs.ts +40 -0
- package/vscode-extension/src/commands/pipeline.ts +103 -0
- package/vscode-extension/src/commands/server.ts +50 -0
- package/vscode-extension/src/commands/smith.ts +112 -0
- package/vscode-extension/src/commands/task.ts +43 -0
- package/vscode-extension/src/commands/terminal.ts +279 -0
- package/vscode-extension/src/commands/workspace.ts +138 -0
- package/vscode-extension/src/connection/manager.ts +80 -0
- package/vscode-extension/src/docs/fs-provider.ts +94 -0
- package/vscode-extension/src/docs/result-provider.ts +33 -0
- package/vscode-extension/src/docs/transport.ts +22 -0
- package/vscode-extension/src/extension.ts +314 -0
- package/vscode-extension/src/statusbar.ts +70 -0
- package/vscode-extension/src/terminal/pseudoterm.ts +123 -0
- package/vscode-extension/src/views/docs.ts +145 -0
- package/vscode-extension/src/views/pipelines.ts +222 -0
- package/vscode-extension/src/views/terminals.ts +91 -0
- package/vscode-extension/src/views/workspaces.ts +243 -0
- 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>
|
package/lib/agents/index.ts
CHANGED
|
@@ -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: '
|
|
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/
|
|
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).
|
package/lib/help-docs/CLAUDE.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
package/lib/workspace/types.ts
CHANGED
|
@@ -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[];
|