@aion0/forge 0.5.44 → 0.5.47
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 +3 -15
- package/middleware.ts +3 -1
- package/package.json +1 -1
- package/tsconfig.json +3 -1
- package/intellij-plugin/README.md +0 -53
- package/intellij-plugin/build.gradle.kts +0 -60
- package/intellij-plugin/gradle/gradle-daemon-jvm.properties +0 -12
- package/intellij-plugin/gradle.properties +0 -9
- package/intellij-plugin/publish.sh +0 -78
- package/intellij-plugin/settings.gradle.kts +0 -7
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/action/LoginAction.kt +0 -49
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/action/LogoutAction.kt +0 -18
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/action/OpenWebUIAction.kt +0 -13
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/action/SwitchConnectionAction.kt +0 -26
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/api/ForgeClient.kt +0 -115
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/auth/Auth.kt +0 -31
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/connection/ConnectionManager.kt +0 -95
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/settings/ForgeConfigurable.kt +0 -81
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/DocsView.kt +0 -99
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/ForgeStatusBarWidgetFactory.kt +0 -94
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/ForgeToolWindowFactory.kt +0 -27
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/ForgeTreeView.kt +0 -176
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/Helpers.kt +0 -48
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/PipelinesView.kt +0 -226
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/TerminalsView.kt +0 -309
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/TreeNodeData.kt +0 -33
- package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/WorkspacesView.kt +0 -166
- package/intellij-plugin/src/main/resources/META-INF/plugin.xml +0 -88
- package/intellij-plugin/src/main/resources/icons/forge.svg +0 -3
- package/vscode-extension/.vscodeignore +0 -11
- package/vscode-extension/README.md +0 -48
- package/vscode-extension/media/icon.png +0 -0
- package/vscode-extension/media/icon.svg +0 -3
- package/vscode-extension/package-lock.json +0 -4046
- package/vscode-extension/package.json +0 -514
- package/vscode-extension/publish.sh +0 -49
- package/vscode-extension/src/api/client.ts +0 -217
- package/vscode-extension/src/auth/auth.ts +0 -32
- package/vscode-extension/src/commands/auth.ts +0 -44
- package/vscode-extension/src/commands/connection.ts +0 -113
- package/vscode-extension/src/commands/docs.ts +0 -40
- package/vscode-extension/src/commands/pipeline.ts +0 -103
- package/vscode-extension/src/commands/server.ts +0 -50
- package/vscode-extension/src/commands/smith.ts +0 -112
- package/vscode-extension/src/commands/task.ts +0 -43
- package/vscode-extension/src/commands/terminal.ts +0 -279
- package/vscode-extension/src/commands/workspace.ts +0 -138
- package/vscode-extension/src/connection/manager.ts +0 -80
- package/vscode-extension/src/docs/fs-provider.ts +0 -94
- package/vscode-extension/src/docs/result-provider.ts +0 -33
- package/vscode-extension/src/docs/transport.ts +0 -22
- package/vscode-extension/src/extension.ts +0 -314
- package/vscode-extension/src/statusbar.ts +0 -70
- package/vscode-extension/src/terminal/pseudoterm.ts +0 -123
- package/vscode-extension/src/views/docs.ts +0 -145
- package/vscode-extension/src/views/pipelines.ts +0 -222
- package/vscode-extension/src/views/terminals.ts +0 -91
- package/vscode-extension/src/views/workspaces.ts +0 -243
- package/vscode-extension/tsconfig.json +0 -16
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import * as vscode from 'vscode';
|
|
2
|
-
import { ForgeClient } from '../api/client';
|
|
3
|
-
import { ForgePty } from '../terminal/pseudoterm';
|
|
4
|
-
|
|
5
|
-
export interface SmithArg {
|
|
6
|
-
workspaceId: string;
|
|
7
|
-
agentId: string;
|
|
8
|
-
label?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** Cache of VSCode terminals we created, keyed by tmux session name. Reusing
|
|
12
|
-
* prevents N duplicate panes when the user clicks the same smith repeatedly.
|
|
13
|
-
* Entries are removed on terminal close (see registerSmithTerminalCleanup). */
|
|
14
|
-
const openedTerminals = new Map<string, vscode.Terminal>();
|
|
15
|
-
|
|
16
|
-
/** Wire VSCode's terminal-close event so we drop stale cache entries. Called
|
|
17
|
-
* once from extension activation. */
|
|
18
|
-
export function registerSmithTerminalCleanup(context: vscode.ExtensionContext): void {
|
|
19
|
-
context.subscriptions.push(vscode.window.onDidCloseTerminal((t) => {
|
|
20
|
-
for (const [key, term] of openedTerminals) {
|
|
21
|
-
if (term === t) {
|
|
22
|
-
openedTerminals.delete(key);
|
|
23
|
-
break;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Open a smith's tmux session in VSCode terminal panel. If we already opened
|
|
30
|
-
* it earlier, just .show() the existing pane instead of creating a new one. */
|
|
31
|
-
export async function smithOpenTerminalCommand(client: ForgeClient, arg?: SmithArg): Promise<void> {
|
|
32
|
-
if (!arg) return;
|
|
33
|
-
const r = await client.wsAction(arg.workspaceId, 'open_terminal', { agentId: arg.agentId });
|
|
34
|
-
if (!r.ok) {
|
|
35
|
-
vscode.window.showErrorMessage(`Forge: open terminal failed — ${r.error}`);
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const tmuxSession: string | undefined = r.data?.tmuxSession;
|
|
39
|
-
if (!tmuxSession) {
|
|
40
|
-
vscode.window.showErrorMessage(`Forge: smith ${arg.label || arg.agentId} has no tmux session yet — try Start Daemon first.`);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const existing = openedTerminals.get(tmuxSession);
|
|
45
|
-
if (existing) {
|
|
46
|
-
existing.show();
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const pty = new ForgePty({ url: client.terminalUrl, attach: tmuxSession });
|
|
51
|
-
const term = vscode.window.createTerminal({ name: `forge: ${arg.label || arg.agentId}`, pty });
|
|
52
|
-
openedTerminals.set(tmuxSession, term);
|
|
53
|
-
term.show();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ── Lifecycle actions ──────────────────────────────────
|
|
57
|
-
|
|
58
|
-
export async function smithPauseCommand(client: ForgeClient, arg?: SmithArg) {
|
|
59
|
-
if (!arg) return;
|
|
60
|
-
const r = await client.wsAction(arg.workspaceId, 'pause', { agentId: arg.agentId });
|
|
61
|
-
reportResult('Pause', r);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export async function smithResumeCommand(client: ForgeClient, arg?: SmithArg) {
|
|
65
|
-
if (!arg) return;
|
|
66
|
-
const r = await client.wsAction(arg.workspaceId, 'resume', { agentId: arg.agentId });
|
|
67
|
-
reportResult('Resume', r);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export async function smithMarkDoneCommand(client: ForgeClient, arg?: SmithArg) {
|
|
71
|
-
if (!arg) return;
|
|
72
|
-
const r = await client.wsAction(arg.workspaceId, 'mark_done', { agentId: arg.agentId, notify: true });
|
|
73
|
-
reportResult('Mark Done', r);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export async function smithMarkFailedCommand(client: ForgeClient, arg?: SmithArg) {
|
|
77
|
-
if (!arg) return;
|
|
78
|
-
const r = await client.wsAction(arg.workspaceId, 'mark_failed', { agentId: arg.agentId, notify: true });
|
|
79
|
-
reportResult('Mark Failed', r);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export async function smithMarkIdleCommand(client: ForgeClient, arg?: SmithArg) {
|
|
83
|
-
if (!arg) return;
|
|
84
|
-
const r = await client.wsAction(arg.workspaceId, 'mark_done', { agentId: arg.agentId, notify: false });
|
|
85
|
-
reportResult('Mark Idle', r);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export async function smithRetryCommand(client: ForgeClient, arg?: SmithArg) {
|
|
89
|
-
if (!arg) return;
|
|
90
|
-
const r = await client.wsAction(arg.workspaceId, 'retry', { agentId: arg.agentId });
|
|
91
|
-
reportResult('Retry', r);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export async function smithSendMessageCommand(client: ForgeClient, arg?: SmithArg) {
|
|
95
|
-
if (!arg) return;
|
|
96
|
-
const content = await vscode.window.showInputBox({
|
|
97
|
-
prompt: `Send message to ${arg.label || arg.agentId}`,
|
|
98
|
-
placeHolder: 'Type your instruction…',
|
|
99
|
-
ignoreFocusOut: true,
|
|
100
|
-
});
|
|
101
|
-
if (!content) return;
|
|
102
|
-
const r = await client.wsAction(arg.workspaceId, 'message', { agentId: arg.agentId, content });
|
|
103
|
-
reportResult('Send Message', r);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function reportResult(action: string, r: { ok: boolean; error?: string }) {
|
|
107
|
-
if (r.ok) {
|
|
108
|
-
vscode.commands.executeCommand('forge.refresh');
|
|
109
|
-
} else {
|
|
110
|
-
vscode.window.showErrorMessage(`Forge: ${action} failed — ${r.error}`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import * as vscode from 'vscode';
|
|
2
|
-
import { ForgeClient } from '../api/client';
|
|
3
|
-
|
|
4
|
-
export async function newTaskCommand(client: ForgeClient): Promise<void> {
|
|
5
|
-
const projRes = await client.listProjects();
|
|
6
|
-
const projects: any[] = projRes.ok && Array.isArray(projRes.data) ? projRes.data : [];
|
|
7
|
-
if (projects.length === 0) {
|
|
8
|
-
vscode.window.showWarningMessage('Forge: no projects configured. Add one in Settings → Project Roots.');
|
|
9
|
-
return;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const projItems = projects.map(p => ({
|
|
13
|
-
label: p.name,
|
|
14
|
-
description: p.path,
|
|
15
|
-
project: p,
|
|
16
|
-
}));
|
|
17
|
-
const proj = await vscode.window.showQuickPick(projItems, { placeHolder: 'Project for this task' });
|
|
18
|
-
if (!proj) return;
|
|
19
|
-
|
|
20
|
-
const prompt = await vscode.window.showInputBox({
|
|
21
|
-
prompt: `Task prompt for ${proj.label}`,
|
|
22
|
-
placeHolder: 'e.g. Add unit tests for the auth module',
|
|
23
|
-
ignoreFocusOut: true,
|
|
24
|
-
});
|
|
25
|
-
if (!prompt) return;
|
|
26
|
-
|
|
27
|
-
const newSessionPick = await vscode.window.showQuickPick(
|
|
28
|
-
[
|
|
29
|
-
{ label: 'Continue last session', value: false },
|
|
30
|
-
{ label: 'Fresh session', value: true },
|
|
31
|
-
],
|
|
32
|
-
{ placeHolder: 'Session mode' },
|
|
33
|
-
);
|
|
34
|
-
if (newSessionPick === undefined) return;
|
|
35
|
-
|
|
36
|
-
const r = await client.createTask(proj.label, prompt, { newSession: newSessionPick.value });
|
|
37
|
-
if (r.ok) {
|
|
38
|
-
vscode.window.showInformationMessage(`Forge: task queued${r.data?.id ? ' (id ' + r.data.id + ')' : ''}.`);
|
|
39
|
-
vscode.commands.executeCommand('forge.refresh');
|
|
40
|
-
} else {
|
|
41
|
-
vscode.window.showErrorMessage(`Forge: failed to queue task — ${r.error}`);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
import * as vscode from 'vscode';
|
|
2
|
-
import { ForgeClient } from '../api/client';
|
|
3
|
-
import { ForgePty } from '../terminal/pseudoterm';
|
|
4
|
-
|
|
5
|
-
export async function openTerminalCommand(client: ForgeClient): Promise<void> {
|
|
6
|
-
const reachable = await client.ping();
|
|
7
|
-
if (!reachable) {
|
|
8
|
-
vscode.window.showErrorMessage('Forge: server unreachable. Run "Forge: Start Server" or check settings.');
|
|
9
|
-
return;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Pick: attach to existing session, or create a new one for a project
|
|
13
|
-
const termsRes = await client.listTerminals();
|
|
14
|
-
const terms: any[] = termsRes.ok && termsRes.data
|
|
15
|
-
? (Array.isArray(termsRes.data) ? termsRes.data : (termsRes.data.sessions || []))
|
|
16
|
-
: [];
|
|
17
|
-
const projRes = await client.listProjects();
|
|
18
|
-
const projects: any[] = projRes.ok && Array.isArray(projRes.data) ? projRes.data : [];
|
|
19
|
-
|
|
20
|
-
interface OpenPickItem extends vscode.QuickPickItem {
|
|
21
|
-
action: 'attach' | 'create';
|
|
22
|
-
sessionName?: string;
|
|
23
|
-
projectPath?: string;
|
|
24
|
-
}
|
|
25
|
-
const items: OpenPickItem[] = [];
|
|
26
|
-
|
|
27
|
-
for (const t of terms) {
|
|
28
|
-
const name = t.name || t.sessionName;
|
|
29
|
-
if (!name) continue;
|
|
30
|
-
items.push({
|
|
31
|
-
label: `$(terminal) ${name}`,
|
|
32
|
-
description: t.projectPath ? `attach · ${t.projectPath}` : 'attach',
|
|
33
|
-
action: 'attach',
|
|
34
|
-
sessionName: name,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
for (const p of projects) {
|
|
38
|
-
items.push({
|
|
39
|
-
label: `$(plus) New terminal in ${p.name}`,
|
|
40
|
-
description: p.path,
|
|
41
|
-
action: 'create',
|
|
42
|
-
projectPath: p.path,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (items.length === 0) {
|
|
47
|
-
vscode.window.showInformationMessage('Forge: no projects configured. Add one in Settings → Project Roots.');
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const picked = await vscode.window.showQuickPick(items, { placeHolder: 'Attach or create a Forge terminal' });
|
|
52
|
-
if (!picked) return;
|
|
53
|
-
|
|
54
|
-
if (picked.action === 'attach' && picked.sessionName) {
|
|
55
|
-
attachTerminalImpl(client, picked.sessionName);
|
|
56
|
-
} else if (picked.action === 'create' && picked.projectPath) {
|
|
57
|
-
return openSessionCommand(client, { projectPath: picked.projectPath, projectName: picked.projectPath.split('/').pop() });
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export async function attachTerminalCommand(client: ForgeClient, arg?: { sessionName?: string }): Promise<void> {
|
|
62
|
-
if (!arg?.sessionName) {
|
|
63
|
-
return openTerminalCommand(client);
|
|
64
|
-
}
|
|
65
|
-
attachTerminalImpl(client, arg.sessionName);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function attachTerminalImpl(client: ForgeClient, sessionName: string): void {
|
|
69
|
-
const pty = new ForgePty({ url: client.terminalUrl, attach: sessionName });
|
|
70
|
-
const term = vscode.window.createTerminal({ name: `forge: ${sessionName}`, pty });
|
|
71
|
-
term.show();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Open a forge session picker for a project — same flow as web UI's terminal picker:
|
|
75
|
-
* Agent → Current Session / New Session / Other Session. Then launches the chosen
|
|
76
|
-
* agent in a tmux. */
|
|
77
|
-
export async function openSessionCommand(client: ForgeClient, arg?: { projectPath?: string; projectName?: string; agentId?: string }): Promise<void> {
|
|
78
|
-
let projectPath = arg?.projectPath;
|
|
79
|
-
let projectName = arg?.projectName;
|
|
80
|
-
let agentId = arg?.agentId;
|
|
81
|
-
|
|
82
|
-
if (!projectPath) {
|
|
83
|
-
const projRes = await client.listProjects();
|
|
84
|
-
const projects: any[] = projRes.ok && Array.isArray(projRes.data) ? projRes.data : [];
|
|
85
|
-
if (projects.length === 0) {
|
|
86
|
-
vscode.window.showInformationMessage('Forge: no projects configured. Add one in Settings → Project Roots.');
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
const picked = await vscode.window.showQuickPick(
|
|
90
|
-
projects.map(p => ({ label: p.name, description: p.path, project: p })),
|
|
91
|
-
{ placeHolder: 'Select project' },
|
|
92
|
-
);
|
|
93
|
-
if (!picked) return;
|
|
94
|
-
projectPath = picked.project.path;
|
|
95
|
-
projectName = picked.project.name;
|
|
96
|
-
}
|
|
97
|
-
if (!projectName && projectPath) projectName = projectPath.split('/').pop();
|
|
98
|
-
|
|
99
|
-
// Pick agent if not provided — only ask when there are 2+ enabled agents.
|
|
100
|
-
if (!agentId) {
|
|
101
|
-
const agentsRes = await client.listAgents();
|
|
102
|
-
const allAgents: any[] = agentsRes.ok && Array.isArray(agentsRes.data?.agents) ? agentsRes.data!.agents : [];
|
|
103
|
-
const defaultAgent = agentsRes.ok ? agentsRes.data?.defaultAgent : undefined;
|
|
104
|
-
const enabled = allAgents.filter(a => a.enabled !== false);
|
|
105
|
-
if (enabled.length === 0) {
|
|
106
|
-
// Fall back to claude if nothing detected — better than refusing.
|
|
107
|
-
agentId = 'claude';
|
|
108
|
-
} else if (enabled.length === 1) {
|
|
109
|
-
agentId = enabled[0].id;
|
|
110
|
-
} else {
|
|
111
|
-
// Place default first, then the rest.
|
|
112
|
-
const sorted = [
|
|
113
|
-
...enabled.filter(a => a.id === defaultAgent),
|
|
114
|
-
...enabled.filter(a => a.id !== defaultAgent),
|
|
115
|
-
];
|
|
116
|
-
const picks = sorted.map(a => ({
|
|
117
|
-
label: `${iconForAgent(a.cliType || a.type)} ${a.name || a.id}`,
|
|
118
|
-
description: a.id === defaultAgent ? 'default' : (a.cliType || a.type || ''),
|
|
119
|
-
detail: a.path && a.path !== a.id ? a.path : undefined,
|
|
120
|
-
agent: a,
|
|
121
|
-
}));
|
|
122
|
-
const sel = await vscode.window.showQuickPick(picks, { placeHolder: 'Select agent' });
|
|
123
|
-
if (!sel) return;
|
|
124
|
-
agentId = sel.agent.id;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Resolve agent launch info — gives us cliCmd, supportsSession, model, env.
|
|
129
|
-
const resolveRes = await client.resolveAgent(agentId!);
|
|
130
|
-
const cliCmd = resolveRes.ok ? (resolveRes.data?.cliCmd || 'claude') : 'claude';
|
|
131
|
-
const supportsSession = resolveRes.ok ? (resolveRes.data?.supportsSession ?? true) : true;
|
|
132
|
-
const cliType = resolveRes.ok ? (resolveRes.data?.cliType || 'claude-code') : 'claude-code';
|
|
133
|
-
const model = resolveRes.ok ? resolveRes.data?.model : undefined;
|
|
134
|
-
const env = resolveRes.ok ? (resolveRes.data?.env || {}) : {};
|
|
135
|
-
|
|
136
|
-
// Resolve the project's bound fixedSession (if any). Only meaningful when
|
|
137
|
-
// the agent supports --resume.
|
|
138
|
-
let fixedSessionId: string | null = null;
|
|
139
|
-
if (supportsSession) {
|
|
140
|
-
try {
|
|
141
|
-
const r = await client.getProjectSession(projectPath!);
|
|
142
|
-
if (r.ok && r.data?.fixedSessionId) fixedSessionId = r.data.fixedSessionId;
|
|
143
|
-
} catch {}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// For agents that don't support --resume (codex, aider, custom), skip the
|
|
147
|
-
// session picker entirely — there's nothing to resume.
|
|
148
|
-
let resumeId: string | null = null;
|
|
149
|
-
if (supportsSession) {
|
|
150
|
-
interface SessionPick extends vscode.QuickPickItem {
|
|
151
|
-
mode: 'current' | 'new' | 'other';
|
|
152
|
-
}
|
|
153
|
-
const picks: SessionPick[] = [];
|
|
154
|
-
if (fixedSessionId) {
|
|
155
|
-
picks.push({
|
|
156
|
-
label: `$(circle-large-filled) Current Session`,
|
|
157
|
-
description: fixedSessionId.slice(0, 16) + '…',
|
|
158
|
-
detail: 'Resume the bound session for this project',
|
|
159
|
-
mode: 'current',
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
picks.push({
|
|
163
|
-
label: `$(add) New Session`,
|
|
164
|
-
detail: `Start a fresh ${cliCmd} session`,
|
|
165
|
-
mode: 'new',
|
|
166
|
-
});
|
|
167
|
-
if (cliType === 'claude-code') {
|
|
168
|
-
// Only claude-code has on-disk session files we can list.
|
|
169
|
-
picks.push({
|
|
170
|
-
label: `$(history) Other Session…`,
|
|
171
|
-
detail: 'Pick from recent claude sessions for this project',
|
|
172
|
-
mode: 'other',
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const choice = await vscode.window.showQuickPick(picks, {
|
|
177
|
-
placeHolder: `Open ${projectName} (${cliCmd}) — choose a session`,
|
|
178
|
-
});
|
|
179
|
-
if (!choice) return;
|
|
180
|
-
|
|
181
|
-
if (choice.mode === 'current' && fixedSessionId) {
|
|
182
|
-
resumeId = fixedSessionId;
|
|
183
|
-
} else if (choice.mode === 'other') {
|
|
184
|
-
if (!projectName) return;
|
|
185
|
-
const sessRes = await client.listClaudeSessions(projectName);
|
|
186
|
-
const sessions: any[] = sessRes.ok && Array.isArray(sessRes.data) ? sessRes.data : [];
|
|
187
|
-
if (sessions.length === 0) {
|
|
188
|
-
vscode.window.showInformationMessage(`Forge: no past sessions found for ${projectName}.`);
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
const otherPicks = sessions.map((s: any, i: number) => {
|
|
192
|
-
const sid = s.sessionId || s.id || '';
|
|
193
|
-
const date = s.modified ? new Date(s.modified).toLocaleString() : '';
|
|
194
|
-
const isCurrent = sid === fixedSessionId;
|
|
195
|
-
const isLatest = i === 0;
|
|
196
|
-
const tag = isCurrent ? ' (current)' : isLatest ? ' (latest)' : '';
|
|
197
|
-
return {
|
|
198
|
-
label: `$(comment-discussion) ${sid.slice(0, 16)}…${tag}`,
|
|
199
|
-
description: date,
|
|
200
|
-
sessionId: sid,
|
|
201
|
-
};
|
|
202
|
-
});
|
|
203
|
-
const sel = await vscode.window.showQuickPick(otherPicks, {
|
|
204
|
-
placeHolder: `Recent sessions in ${projectName}`,
|
|
205
|
-
});
|
|
206
|
-
if (!sel) return;
|
|
207
|
-
resumeId = sel.sessionId;
|
|
208
|
-
}
|
|
209
|
-
// mode === 'new' → resumeId stays null
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Build launch command. Only claude-code uses --dangerously-skip-permissions
|
|
213
|
-
// by default; other CLIs have their own (e.g. codex --full-auto) that we
|
|
214
|
-
// don't auto-add — user can configure their own settings.
|
|
215
|
-
const resumeFlag = resumeId ? ` --resume ${resumeId}` : '';
|
|
216
|
-
const modelFlag = model ? ` --model ${model}` : '';
|
|
217
|
-
const skipFlag = cliType === 'claude-code' ? ' --dangerously-skip-permissions' : '';
|
|
218
|
-
const envExports = Object.entries(env)
|
|
219
|
-
.filter(([k]) => k !== 'CLAUDE_MODEL') // model passed via --model
|
|
220
|
-
.map(([k, v]) => `export ${k}="${v}"`)
|
|
221
|
-
.join(' && ');
|
|
222
|
-
const envPrefix = envExports ? envExports + ' && ' : '';
|
|
223
|
-
const launchCommand = `${envPrefix}cd "${projectPath}" && ${cliCmd}${resumeFlag}${modelFlag}${skipFlag}`;
|
|
224
|
-
|
|
225
|
-
const pty = new ForgePty({ url: client.terminalUrl, cwd: projectPath, launchCommand });
|
|
226
|
-
const label = projectName || 'forge';
|
|
227
|
-
const agentTag = cliCmd !== 'claude' ? ` (${cliCmd})` : '';
|
|
228
|
-
const term = vscode.window.createTerminal({ name: `forge: ${label}${agentTag}`, pty });
|
|
229
|
-
term.show();
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function iconForAgent(cliType: string | undefined): string {
|
|
233
|
-
switch (cliType) {
|
|
234
|
-
case 'claude-code': return '$(comment-discussion)';
|
|
235
|
-
case 'codex': return '$(zap)';
|
|
236
|
-
case 'aider': return '$(edit)';
|
|
237
|
-
default: return '$(robot)';
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** Backwards-compatible alias kept for the tree view "New Terminal" entries. */
|
|
242
|
-
export const newTerminalCommand = openSessionCommand;
|
|
243
|
-
|
|
244
|
-
export async function sendSelectionCommand(client: ForgeClient): Promise<void> {
|
|
245
|
-
const editor = vscode.window.activeTextEditor;
|
|
246
|
-
if (!editor || editor.selection.isEmpty) {
|
|
247
|
-
vscode.window.showWarningMessage('Forge: no text selected.');
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
const text = editor.document.getText(editor.selection);
|
|
251
|
-
|
|
252
|
-
const termsRes = await client.listTerminals();
|
|
253
|
-
const terms: any[] = termsRes.ok && termsRes.data
|
|
254
|
-
? (Array.isArray(termsRes.data) ? termsRes.data : (termsRes.data.sessions || []))
|
|
255
|
-
: [];
|
|
256
|
-
if (terms.length === 0) {
|
|
257
|
-
vscode.window.showInformationMessage('Forge: no active terminals.');
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
const items: (vscode.QuickPickItem & { name: string })[] = terms
|
|
261
|
-
.map((t: any) => ({
|
|
262
|
-
label: `$(terminal) ${t.name || t.sessionName}`,
|
|
263
|
-
description: t.projectPath || '',
|
|
264
|
-
name: t.name || t.sessionName,
|
|
265
|
-
}))
|
|
266
|
-
.filter(i => i.name);
|
|
267
|
-
const picked = await vscode.window.showQuickPick(items, { placeHolder: 'Send selection to which terminal?' });
|
|
268
|
-
if (!picked) return;
|
|
269
|
-
|
|
270
|
-
const r = await client.request(`/api/terminal/inject`, {
|
|
271
|
-
method: 'POST',
|
|
272
|
-
body: JSON.stringify({ sessionName: picked.name, text }),
|
|
273
|
-
});
|
|
274
|
-
if (r.ok) {
|
|
275
|
-
vscode.window.showInformationMessage(`Forge: sent ${text.length} chars to ${picked.name}`);
|
|
276
|
-
} else {
|
|
277
|
-
vscode.window.showErrorMessage(`Forge inject failed: ${r.error}`);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import * as vscode from 'vscode';
|
|
2
|
-
import { ForgeClient } from '../api/client';
|
|
3
|
-
|
|
4
|
-
/** Open the forge workspace bound to the current VSCode folder.
|
|
5
|
-
* - If found: focus the forge sidebar so the user sees it.
|
|
6
|
-
* - If not found: ask whether to create one. */
|
|
7
|
-
export async function openWorkspaceForFolderCommand(client: ForgeClient): Promise<void> {
|
|
8
|
-
const folders = vscode.workspace.workspaceFolders;
|
|
9
|
-
if (!folders || folders.length === 0) {
|
|
10
|
-
vscode.window.showWarningMessage('Forge: no folder open in VSCode.');
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// If user has multiple folders, ask which.
|
|
15
|
-
let folder = folders[0];
|
|
16
|
-
if (folders.length > 1) {
|
|
17
|
-
const picked = await vscode.window.showQuickPick(
|
|
18
|
-
folders.map(f => ({ label: f.name, description: f.uri.fsPath, folder: f })),
|
|
19
|
-
{ placeHolder: 'Which folder?' },
|
|
20
|
-
);
|
|
21
|
-
if (!picked) return;
|
|
22
|
-
folder = picked.folder;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const projectPath = folder.uri.fsPath;
|
|
26
|
-
const r = await client.findWorkspaceByPath(projectPath);
|
|
27
|
-
|
|
28
|
-
if (r.ok && r.data) {
|
|
29
|
-
// Existing workspace — focus the sidebar.
|
|
30
|
-
vscode.commands.executeCommand('workbench.view.extension.forge');
|
|
31
|
-
vscode.commands.executeCommand('forge.refresh');
|
|
32
|
-
vscode.window.setStatusBarMessage(`Forge: ${r.data.projectName} workspace ready`, 3000);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// No workspace yet — offer to create one.
|
|
37
|
-
const choice = await vscode.window.showInformationMessage(
|
|
38
|
-
`No forge workspace for ${folder.name}. Create one?`,
|
|
39
|
-
'Create', 'Cancel',
|
|
40
|
-
);
|
|
41
|
-
if (choice !== 'Create') return;
|
|
42
|
-
|
|
43
|
-
const create = await client.request('/api/workspace', {
|
|
44
|
-
method: 'POST',
|
|
45
|
-
body: JSON.stringify({ projectPath, projectName: folder.name }),
|
|
46
|
-
});
|
|
47
|
-
if (create.ok) {
|
|
48
|
-
vscode.window.showInformationMessage(`Forge: workspace created for ${folder.name}.`);
|
|
49
|
-
vscode.commands.executeCommand('workbench.view.extension.forge');
|
|
50
|
-
vscode.commands.executeCommand('forge.refresh');
|
|
51
|
-
} else {
|
|
52
|
-
vscode.window.showErrorMessage(`Forge: failed to create workspace — ${create.error}`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Pick from the list of existing workspaces. */
|
|
57
|
-
export async function openWorkspaceCommand(client: ForgeClient): Promise<void> {
|
|
58
|
-
const r = await client.listWorkspaces();
|
|
59
|
-
if (!r.ok || !Array.isArray(r.data) || r.data.length === 0) {
|
|
60
|
-
vscode.window.showInformationMessage('Forge: no workspaces yet. Use "Open Workspace for Current Folder" to create one.');
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const items = r.data.map((ws: any) => ({
|
|
64
|
-
label: ws.projectName || ws.id,
|
|
65
|
-
description: ws.projectPath || '',
|
|
66
|
-
detail: `${ws.agentCount || 0} smith(s)`,
|
|
67
|
-
workspace: ws,
|
|
68
|
-
}));
|
|
69
|
-
const picked = await vscode.window.showQuickPick(items, { placeHolder: 'Select a workspace' });
|
|
70
|
-
if (!picked) return;
|
|
71
|
-
vscode.commands.executeCommand('workbench.view.extension.forge');
|
|
72
|
-
vscode.commands.executeCommand('forge.refresh');
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ── Daemon control ──────────────────────────────────────
|
|
76
|
-
|
|
77
|
-
export async function startDaemonCommand(client: ForgeClient, arg?: { workspaceId?: string }): Promise<void> {
|
|
78
|
-
const id = await resolveWorkspaceId(client, arg?.workspaceId);
|
|
79
|
-
if (!id) return;
|
|
80
|
-
const r = await client.wsAction(id, 'start_daemon');
|
|
81
|
-
if (r.ok) {
|
|
82
|
-
vscode.window.showInformationMessage('Forge: daemon starting…');
|
|
83
|
-
vscode.commands.executeCommand('forge.refresh');
|
|
84
|
-
} else {
|
|
85
|
-
vscode.window.showErrorMessage(`Forge: start daemon failed — ${r.error}`);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export async function stopDaemonCommand(client: ForgeClient, arg?: { workspaceId?: string }): Promise<void> {
|
|
90
|
-
const id = await resolveWorkspaceId(client, arg?.workspaceId);
|
|
91
|
-
if (!id) return;
|
|
92
|
-
const choice = await vscode.window.showWarningMessage(
|
|
93
|
-
'Stop the workspace daemon? Running smiths will be terminated.',
|
|
94
|
-
'Stop', 'Cancel',
|
|
95
|
-
);
|
|
96
|
-
if (choice !== 'Stop') return;
|
|
97
|
-
const r = await client.wsAction(id, 'stop_daemon');
|
|
98
|
-
if (r.ok) {
|
|
99
|
-
vscode.window.showInformationMessage('Forge: daemon stopped.');
|
|
100
|
-
vscode.commands.executeCommand('forge.refresh');
|
|
101
|
-
} else {
|
|
102
|
-
vscode.window.showErrorMessage(`Forge: stop daemon failed — ${r.error}`);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export async function restartDaemonCommand(client: ForgeClient, arg?: { workspaceId?: string }): Promise<void> {
|
|
107
|
-
const id = await resolveWorkspaceId(client, arg?.workspaceId);
|
|
108
|
-
if (!id) return;
|
|
109
|
-
const stop = await client.wsAction(id, 'stop_daemon');
|
|
110
|
-
if (!stop.ok) {
|
|
111
|
-
vscode.window.showErrorMessage(`Forge: restart failed at stop step — ${stop.error}`);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
// Brief settle
|
|
115
|
-
await new Promise(r => setTimeout(r, 800));
|
|
116
|
-
const start = await client.wsAction(id, 'start_daemon');
|
|
117
|
-
if (start.ok) {
|
|
118
|
-
vscode.window.showInformationMessage('Forge: daemon restarted.');
|
|
119
|
-
vscode.commands.executeCommand('forge.refresh');
|
|
120
|
-
} else {
|
|
121
|
-
vscode.window.showErrorMessage(`Forge: restart failed at start step — ${start.error}`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function resolveWorkspaceId(client: ForgeClient, given?: string): Promise<string | undefined> {
|
|
126
|
-
if (given) return given;
|
|
127
|
-
const r = await client.listWorkspaces();
|
|
128
|
-
if (!r.ok || !Array.isArray(r.data) || r.data.length === 0) {
|
|
129
|
-
vscode.window.showWarningMessage('Forge: no workspaces.');
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
if (r.data.length === 1) return r.data[0].id;
|
|
133
|
-
const picked = await vscode.window.showQuickPick(
|
|
134
|
-
r.data.map((ws: any) => ({ label: ws.projectName || ws.id, description: ws.projectPath, ws })),
|
|
135
|
-
{ placeHolder: 'Which workspace?' },
|
|
136
|
-
);
|
|
137
|
-
return picked?.ws?.id;
|
|
138
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import * as vscode from 'vscode';
|
|
2
|
-
|
|
3
|
-
export interface ForgeConnection {
|
|
4
|
-
name: string;
|
|
5
|
-
serverUrl: string;
|
|
6
|
-
terminalUrl: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const CONFIG_NS = 'forge';
|
|
10
|
-
|
|
11
|
-
/** Manages the user's saved forge connections and the active one.
|
|
12
|
-
* Persisted in VSCode's user settings (`forge.connections` + `forge.activeConnection`). */
|
|
13
|
-
export class ConnectionManager {
|
|
14
|
-
private _onDidChange = new vscode.EventEmitter<ForgeConnection>();
|
|
15
|
-
onDidChange = this._onDidChange.event;
|
|
16
|
-
|
|
17
|
-
/** Listen for the user editing connections in settings.json so we can
|
|
18
|
-
* refresh dependent state. */
|
|
19
|
-
watchConfig(context: vscode.ExtensionContext): void {
|
|
20
|
-
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => {
|
|
21
|
-
if (e.affectsConfiguration('forge.connections') || e.affectsConfiguration('forge.activeConnection')
|
|
22
|
-
|| e.affectsConfiguration('forge.serverUrl') || e.affectsConfiguration('forge.terminalUrl')) {
|
|
23
|
-
this._onDidChange.fire(this.active());
|
|
24
|
-
}
|
|
25
|
-
}));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
list(): ForgeConnection[] {
|
|
29
|
-
const cfg = vscode.workspace.getConfiguration(CONFIG_NS);
|
|
30
|
-
const conns = cfg.get<ForgeConnection[]>('connections', []);
|
|
31
|
-
if (conns && conns.length > 0) return conns;
|
|
32
|
-
// Backwards compat: synthesize "Local" from legacy single-server fields.
|
|
33
|
-
return [{
|
|
34
|
-
name: 'Local',
|
|
35
|
-
serverUrl: cfg.get<string>('serverUrl', 'http://localhost:8403'),
|
|
36
|
-
terminalUrl: cfg.get<string>('terminalUrl', 'ws://localhost:8404'),
|
|
37
|
-
}];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
active(): ForgeConnection {
|
|
41
|
-
const all = this.list();
|
|
42
|
-
const cfg = vscode.workspace.getConfiguration(CONFIG_NS);
|
|
43
|
-
const name = cfg.get<string>('activeConnection');
|
|
44
|
-
return (name && all.find(c => c.name === name)) || all[0];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async setActive(name: string): Promise<void> {
|
|
48
|
-
await vscode.workspace.getConfiguration(CONFIG_NS)
|
|
49
|
-
.update('activeConnection', name, vscode.ConfigurationTarget.Global);
|
|
50
|
-
this._onDidChange.fire(this.active());
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async add(c: ForgeConnection): Promise<void> {
|
|
54
|
-
const cfg = vscode.workspace.getConfiguration(CONFIG_NS);
|
|
55
|
-
let list = cfg.get<ForgeConnection[]>('connections', []);
|
|
56
|
-
if (list.length === 0) {
|
|
57
|
-
// Promote the synthesized Local entry so the user-set list is explicit.
|
|
58
|
-
list = this.list();
|
|
59
|
-
}
|
|
60
|
-
if (list.some(x => x.name === c.name)) {
|
|
61
|
-
throw new Error(`A connection named "${c.name}" already exists`);
|
|
62
|
-
}
|
|
63
|
-
list.push(c);
|
|
64
|
-
await cfg.update('connections', list, vscode.ConfigurationTarget.Global);
|
|
65
|
-
this._onDidChange.fire(this.active());
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async remove(name: string): Promise<void> {
|
|
69
|
-
const cfg = vscode.workspace.getConfiguration(CONFIG_NS);
|
|
70
|
-
const list = cfg.get<ForgeConnection[]>('connections', []).filter(c => c.name !== name);
|
|
71
|
-
await cfg.update('connections', list, vscode.ConfigurationTarget.Global);
|
|
72
|
-
// If we removed the active one, fall back to the first remaining.
|
|
73
|
-
const activeName = cfg.get<string>('activeConnection');
|
|
74
|
-
if (activeName === name) {
|
|
75
|
-
const remaining = this.list();
|
|
76
|
-
await cfg.update('activeConnection', remaining[0]?.name, vscode.ConfigurationTarget.Global);
|
|
77
|
-
}
|
|
78
|
-
this._onDidChange.fire(this.active());
|
|
79
|
-
}
|
|
80
|
-
}
|