@runfusion/fusion 0.7.1 → 0.8.1
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/dist/bin.js +3737 -1373
- package/dist/client/assets/{AgentDetailView-DyzuiJas.js → AgentDetailView-CLzxf6Z7.js} +3 -3
- package/dist/client/assets/{AgentDetailView-C1_lTTET.css → AgentDetailView-DIBOY8V-.css} +1 -1
- package/dist/client/assets/{AgentsView-CgweOTe6.js → AgentsView-CXaYJX_G.js} +3 -3
- package/dist/client/assets/{ChatView-DrY8FMIt.js → ChatView-iXxGAaN1.js} +1 -1
- package/dist/client/assets/DevServerView-BeXfFkF4.js +1 -0
- package/dist/client/assets/{DirectoryPicker-D5KQ-im_.js → DirectoryPicker-BMn5fjn9.js} +1 -1
- package/dist/client/assets/{DocumentsView-D2wK7FYJ.js → DocumentsView-CjrtI3TX.js} +1 -1
- package/dist/client/assets/{InsightsView-DfY3sa1j.js → InsightsView-BkfQ-TV1.js} +1 -1
- package/dist/client/assets/MemoryView-1G0zWu1i.js +2 -0
- package/dist/client/assets/{NodesView-g26-j7rg.js → NodesView-Bn_1R73N.js} +1 -1
- package/dist/client/assets/{NodesView-BYVG2yY-.css → NodesView-DCoS6iYh.css} +1 -1
- package/dist/client/assets/{PiExtensionsManager-DfMr3Gls.js → PiExtensionsManager-CqGOtQnR.js} +3 -3
- package/dist/client/assets/{PluginManager-DiMOD-Kj.js → PluginManager-CM5QGvSG.js} +1 -1
- package/dist/client/assets/{RoadmapsView-DJC4F4CD.js → RoadmapsView-B4VnQP83.js} +1 -1
- package/dist/client/assets/SettingsModal-BiLA-BeG.js +31 -0
- package/dist/client/assets/{SettingsModal-Cx3iMWDs.js → SettingsModal-C3LckzfT.js} +1 -1
- package/dist/client/assets/SettingsModal-D5hLoLXp.css +1 -0
- package/dist/client/assets/SetupWizardModal-Bk_8HfLm.js +1 -0
- package/dist/client/assets/SetupWizardModal-DRF5fOoR.css +1 -0
- package/dist/client/assets/{SkillsView-DTB2cmXQ.js → SkillsView-CRvqF8P1.js} +1 -1
- package/dist/client/assets/{TodoView-CyxdHUdz.js → TodoView-Vzui5Eha.js} +2 -2
- package/dist/client/assets/{folder-open-C3zB1vmh.js → folder-open-CMF89prE.js} +1 -1
- package/dist/client/assets/index-B8kH5y4Q.js +656 -0
- package/dist/client/assets/index-D2fXOwWF.css +1 -0
- package/dist/client/assets/{list-checks-CK3_6p5e.js → list-checks-M95d1uAy.js} +1 -1
- package/dist/client/assets/{star-BQhDgM9V.js → star-DHhJD6ow.js} +1 -1
- package/dist/client/assets/{upload-DDdZveEJ.js → upload-CEq8jic8.js} +1 -1
- package/dist/client/assets/{users-DWWgd19M.js → users-CUA8Tv-d.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/version.json +1 -1
- package/dist/extension.js +2403 -471
- package/dist/pi-claude-cli/index.ts +27 -0
- package/dist/pi-claude-cli/package.json +4 -5
- package/dist/pi-claude-cli/src/__tests__/process-manager.test.ts +31 -9
- package/dist/pi-claude-cli/src/__tests__/provider.test.ts +122 -8
- package/dist/pi-claude-cli/src/process-manager.ts +25 -7
- package/dist/pi-claude-cli/src/provider.ts +31 -7
- package/dist/pi-claude-cli/src/types/cross-spawn.d.ts +7 -0
- package/package.json +2 -6
- package/skill/fusion/references/extension-tools.md +1 -0
- package/dist/client/assets/DevServerView-fvjo36sF.js +0 -6
- package/dist/client/assets/MemoryView-CyAQgXwO.js +0 -2
- package/dist/client/assets/SettingsModal-BnekMOV2.css +0 -1
- package/dist/client/assets/SettingsModal-DjVE27r5.js +0 -31
- package/dist/client/assets/SetupWizardModal-BMa6p24b.css +0 -1
- package/dist/client/assets/SetupWizardModal-Cow6woq6.js +0 -1
- package/dist/client/assets/index-Belw0PQt.css +0 -1
- package/dist/client/assets/index-DJDWSrju.js +0 -646
|
@@ -144,6 +144,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
144
144
|
contextWindow: 1_000_000,
|
|
145
145
|
maxTokens: 128_000,
|
|
146
146
|
},
|
|
147
|
+
{
|
|
148
|
+
id: "claude-sonnet-4-6",
|
|
149
|
+
name: "Claude Sonnet 4.6",
|
|
150
|
+
reasoning: true,
|
|
151
|
+
input: ["text", "image"],
|
|
152
|
+
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
153
|
+
contextWindow: 200_000,
|
|
154
|
+
maxTokens: 16_384,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: "claude-sonnet-4-5",
|
|
158
|
+
name: "Claude Sonnet 4.5",
|
|
159
|
+
reasoning: true,
|
|
160
|
+
input: ["text", "image"],
|
|
161
|
+
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
162
|
+
contextWindow: 200_000,
|
|
163
|
+
maxTokens: 8_192,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: "claude-haiku-4-5",
|
|
167
|
+
name: "Claude Haiku 4.5",
|
|
168
|
+
reasoning: true,
|
|
169
|
+
input: ["text", "image"],
|
|
170
|
+
cost: { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 },
|
|
171
|
+
contextWindow: 200_000,
|
|
172
|
+
maxTokens: 8_192,
|
|
173
|
+
},
|
|
147
174
|
];
|
|
148
175
|
|
|
149
176
|
const seen = new Set(catalogModels.map((m) => m.id));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion/pi-claude-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Fusion vendored fork: pi coding-agent extension that routes LLM calls through the Claude Code CLI. Forked from rchern/pi-claude-cli (MIT). See UPSTREAM.md.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": true,
|
|
@@ -19,15 +19,14 @@
|
|
|
19
19
|
"url": "https://github.com/Runfusion/Fusion",
|
|
20
20
|
"directory": "packages/pi-claude-cli"
|
|
21
21
|
},
|
|
22
|
-
"dependencies": {
|
|
23
|
-
"cross-spawn": "^7.0.6"
|
|
24
|
-
},
|
|
25
22
|
"peerDependencies": {
|
|
26
23
|
"@mariozechner/pi-ai": "*",
|
|
27
24
|
"@mariozechner/pi-coding-agent": "*"
|
|
28
25
|
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"cross-spawn": "^7.0.6"
|
|
28
|
+
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@types/cross-spawn": "^6.0.6",
|
|
31
30
|
"@types/node": "^22.0.0",
|
|
32
31
|
"typescript": "^5.7.0",
|
|
33
32
|
"vitest": "^3.0.0"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import type { ChildProcess } from "node:child_process";
|
|
3
3
|
|
|
4
|
-
// Mock
|
|
5
|
-
vi.mock("
|
|
6
|
-
|
|
4
|
+
// Mock child_process.spawn before importing process-manager
|
|
5
|
+
vi.mock("node:child_process", () => ({
|
|
6
|
+
spawn: vi.fn(() => {
|
|
7
7
|
const EventEmitter = require("node:events");
|
|
8
8
|
const proc = new EventEmitter();
|
|
9
9
|
proc.stdin = { write: vi.fn(), end: vi.fn() };
|
|
@@ -16,10 +16,6 @@ vi.mock("cross-spawn", () => ({
|
|
|
16
16
|
proc.pid = 12345;
|
|
17
17
|
return proc;
|
|
18
18
|
}),
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
// Mock child_process.execSync for validation tests
|
|
22
|
-
vi.mock("node:child_process", () => ({
|
|
23
19
|
execSync: vi.fn(),
|
|
24
20
|
}));
|
|
25
21
|
|
|
@@ -42,10 +38,10 @@ vi.mock("node:os", () => ({
|
|
|
42
38
|
tmpdir: mocks.tmpdir,
|
|
43
39
|
}));
|
|
44
40
|
|
|
45
|
-
import spawn from "
|
|
46
|
-
import { execSync } from "node:child_process";
|
|
41
|
+
import { spawn, execSync } from "node:child_process";
|
|
47
42
|
import {
|
|
48
43
|
spawnClaude,
|
|
44
|
+
buildClaudeSpawnArgs,
|
|
49
45
|
writeUserMessage,
|
|
50
46
|
cleanupProcess,
|
|
51
47
|
captureStderr,
|
|
@@ -57,6 +53,32 @@ import {
|
|
|
57
53
|
cleanupSystemPromptFile,
|
|
58
54
|
} from "../process-manager";
|
|
59
55
|
|
|
56
|
+
describe("buildClaudeSpawnArgs", () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
vi.clearAllMocks();
|
|
59
|
+
mocks.writeFileSync.mockReset();
|
|
60
|
+
mocks.tmpdir.mockReset();
|
|
61
|
+
mocks.tmpdir.mockReturnValue("/mock-tmp");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("builds args including model and optional session/mcp flags", () => {
|
|
65
|
+
const args = buildClaudeSpawnArgs("claude-sonnet-4-6", undefined, {
|
|
66
|
+
resumeSessionId: "sess-1",
|
|
67
|
+
effort: "high",
|
|
68
|
+
mcpConfigPath: "/tmp/mcp.json",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(args).toContain("--model");
|
|
72
|
+
expect(args).toContain("claude-sonnet-4-6");
|
|
73
|
+
expect(args).toContain("--resume");
|
|
74
|
+
expect(args).toContain("sess-1");
|
|
75
|
+
expect(args).toContain("--effort");
|
|
76
|
+
expect(args).toContain("high");
|
|
77
|
+
expect(args).toContain("--mcp-config");
|
|
78
|
+
expect(args).toContain("/tmp/mcp.json");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
60
82
|
describe("spawnClaude", () => {
|
|
61
83
|
beforeEach(() => {
|
|
62
84
|
vi.clearAllMocks();
|
|
@@ -2,9 +2,9 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import { PassThrough } from "node:stream";
|
|
4
4
|
|
|
5
|
-
// Mock
|
|
6
|
-
vi.mock("
|
|
7
|
-
|
|
5
|
+
// Mock child_process.spawn with PassThrough streams for readline compatibility
|
|
6
|
+
vi.mock("node:child_process", () => ({
|
|
7
|
+
spawn: vi.fn(() => {
|
|
8
8
|
const proc = new EventEmitter();
|
|
9
9
|
const stdin = { write: vi.fn(), end: vi.fn() };
|
|
10
10
|
const stdout = new PassThrough();
|
|
@@ -20,10 +20,6 @@ vi.mock("cross-spawn", () => ({
|
|
|
20
20
|
(proc as any).pid = 99999;
|
|
21
21
|
return proc;
|
|
22
22
|
}),
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
|
-
// Mock child_process.execSync for validateCliPresence/validateCliAuth
|
|
26
|
-
vi.mock("node:child_process", () => ({
|
|
27
23
|
execSync: vi.fn(() => Buffer.from("1.0.0")),
|
|
28
24
|
}));
|
|
29
25
|
|
|
@@ -69,7 +65,8 @@ vi.mock("@mariozechner/pi-ai", () => ({
|
|
|
69
65
|
calculateCost: vi.fn(),
|
|
70
66
|
}));
|
|
71
67
|
|
|
72
|
-
import spawn from "
|
|
68
|
+
import { spawn } from "node:child_process";
|
|
69
|
+
import { getModels } from "@mariozechner/pi-ai";
|
|
73
70
|
import { streamViaCli } from "../provider";
|
|
74
71
|
|
|
75
72
|
describe("provider registration (default export)", () => {
|
|
@@ -119,16 +116,71 @@ describe("provider registration (default export)", () => {
|
|
|
119
116
|
expect(firstModel.maxTokens).toBe(8192);
|
|
120
117
|
expect(firstModel.cost).toBeDefined();
|
|
121
118
|
});
|
|
119
|
+
|
|
120
|
+
it("includes all extra Claude model entries", async () => {
|
|
121
|
+
const registerProvider = vi.fn();
|
|
122
|
+
const mockPi = { registerProvider, on: vi.fn() } as any;
|
|
123
|
+
|
|
124
|
+
const mod = await import("../../index");
|
|
125
|
+
mod.default(mockPi);
|
|
126
|
+
|
|
127
|
+
const config = registerProvider.mock.calls[0][1];
|
|
128
|
+
const modelIds = new Set(config.models.map((m: { id: string }) => m.id));
|
|
129
|
+
|
|
130
|
+
for (const id of [
|
|
131
|
+
"claude-opus-4-7",
|
|
132
|
+
"claude-sonnet-4-6",
|
|
133
|
+
"claude-sonnet-4-5",
|
|
134
|
+
"claude-haiku-4-5",
|
|
135
|
+
]) {
|
|
136
|
+
expect(modelIds.has(id)).toBe(true);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("deduplicates extra models when catalog already includes them", async () => {
|
|
141
|
+
const registerProvider = vi.fn();
|
|
142
|
+
const mockPi = { registerProvider, on: vi.fn() } as any;
|
|
143
|
+
const getModelsMock = vi.mocked(getModels);
|
|
144
|
+
|
|
145
|
+
getModelsMock.mockReturnValueOnce([
|
|
146
|
+
...mockModels,
|
|
147
|
+
{
|
|
148
|
+
id: "claude-sonnet-4-6",
|
|
149
|
+
name: "Claude Sonnet 4.6",
|
|
150
|
+
api: "anthropic",
|
|
151
|
+
provider: "anthropic",
|
|
152
|
+
reasoning: true,
|
|
153
|
+
input: ["text", "image"],
|
|
154
|
+
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
155
|
+
contextWindow: 200000,
|
|
156
|
+
maxTokens: 16384,
|
|
157
|
+
} as any,
|
|
158
|
+
] as any);
|
|
159
|
+
|
|
160
|
+
const mod = await import("../../index");
|
|
161
|
+
mod.default(mockPi);
|
|
162
|
+
|
|
163
|
+
const config = registerProvider.mock.calls[0][1];
|
|
164
|
+
const matches = config.models.filter(
|
|
165
|
+
(m: { id: string }) => m.id === "claude-sonnet-4-6",
|
|
166
|
+
);
|
|
167
|
+
expect(matches).toHaveLength(1);
|
|
168
|
+
});
|
|
122
169
|
});
|
|
123
170
|
|
|
124
171
|
describe("streamViaCli", () => {
|
|
125
172
|
beforeEach(() => {
|
|
126
173
|
vi.clearAllMocks();
|
|
127
174
|
vi.useFakeTimers();
|
|
175
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
176
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
177
|
+
delete process.env.PI_CLAUDE_CLI_DEBUG;
|
|
128
178
|
});
|
|
129
179
|
|
|
130
180
|
afterEach(() => {
|
|
131
181
|
vi.useRealTimers();
|
|
182
|
+
vi.restoreAllMocks();
|
|
183
|
+
delete process.env.PI_CLAUDE_CLI_DEBUG;
|
|
132
184
|
});
|
|
133
185
|
|
|
134
186
|
it("returns an AssistantMessageEventStream", () => {
|
|
@@ -144,6 +196,24 @@ describe("streamViaCli", () => {
|
|
|
144
196
|
expect(result.end).toBeDefined();
|
|
145
197
|
});
|
|
146
198
|
|
|
199
|
+
it("logs PID and spawn args when debug mode is enabled", async () => {
|
|
200
|
+
process.env.PI_CLAUDE_CLI_DEBUG = "1";
|
|
201
|
+
|
|
202
|
+
const model = mockModels[0] as any;
|
|
203
|
+
const context = {
|
|
204
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const errorSpy = vi.spyOn(console, "error");
|
|
208
|
+
|
|
209
|
+
streamViaCli(model, context);
|
|
210
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
211
|
+
|
|
212
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
213
|
+
expect.stringContaining("spawned claude subprocess pid=99999 args="),
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
147
217
|
it("spawns subprocess and writes user message to stdin", async () => {
|
|
148
218
|
const model = mockModels[0] as any;
|
|
149
219
|
const context = {
|
|
@@ -1183,6 +1253,50 @@ describe("streamViaCli", () => {
|
|
|
1183
1253
|
expect(doneEvent.message.content).toBeDefined();
|
|
1184
1254
|
});
|
|
1185
1255
|
|
|
1256
|
+
it("logs stderr at warn level on close even with exit code 0", async () => {
|
|
1257
|
+
const model = mockModels[0] as any;
|
|
1258
|
+
const context = {
|
|
1259
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
const warnSpy = vi.spyOn(console, "warn");
|
|
1263
|
+
|
|
1264
|
+
streamViaCli(model, context);
|
|
1265
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
1266
|
+
|
|
1267
|
+
const proc = (spawn as any).mock.results[0].value;
|
|
1268
|
+
|
|
1269
|
+
proc.stderr.emit("data", Buffer.from("minor warning from cli"));
|
|
1270
|
+
proc.emit("close", 0, null);
|
|
1271
|
+
proc.stdout.end();
|
|
1272
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
1273
|
+
|
|
1274
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
1275
|
+
expect.stringContaining("minor warning from cli"),
|
|
1276
|
+
);
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
it("warns when subprocess closes successfully with no content events", async () => {
|
|
1280
|
+
const model = mockModels[0] as any;
|
|
1281
|
+
const context = {
|
|
1282
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
1283
|
+
};
|
|
1284
|
+
|
|
1285
|
+
const warnSpy = vi.spyOn(console, "warn");
|
|
1286
|
+
|
|
1287
|
+
streamViaCli(model, context);
|
|
1288
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
1289
|
+
|
|
1290
|
+
const proc = (spawn as any).mock.results[0].value;
|
|
1291
|
+
proc.emit("close", 0, null);
|
|
1292
|
+
proc.stdout.end();
|
|
1293
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
1294
|
+
|
|
1295
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
1296
|
+
expect.stringContaining("closed without content events"),
|
|
1297
|
+
);
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1186
1300
|
it("does not push error on normal close (code 0)", async () => {
|
|
1187
1301
|
const model = mockModels[0] as any;
|
|
1188
1302
|
const context = {
|
|
@@ -6,12 +6,10 @@
|
|
|
6
6
|
* Also provides startup validation for CLI presence and authentication.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import spawn from "
|
|
10
|
-
import { execSync } from "node:child_process";
|
|
9
|
+
import { execSync, spawn, type ChildProcess } from "node:child_process";
|
|
11
10
|
import { writeFileSync, unlinkSync } from "node:fs";
|
|
12
11
|
import { join } from "node:path";
|
|
13
12
|
import { tmpdir } from "node:os";
|
|
14
|
-
import type { ChildProcess } from "node:child_process";
|
|
15
13
|
|
|
16
14
|
/**
|
|
17
15
|
* Spawn a Claude CLI subprocess with all required flags for stream-json communication.
|
|
@@ -21,18 +19,16 @@ import type { ChildProcess } from "node:child_process";
|
|
|
21
19
|
* @param options - Optional cwd, AbortSignal, and effort level
|
|
22
20
|
* @returns The spawned ChildProcess with piped stdin/stdout/stderr
|
|
23
21
|
*/
|
|
24
|
-
export function
|
|
22
|
+
export function buildClaudeSpawnArgs(
|
|
25
23
|
modelId: string,
|
|
26
24
|
systemPrompt?: string,
|
|
27
25
|
options?: {
|
|
28
|
-
cwd?: string;
|
|
29
|
-
signal?: AbortSignal;
|
|
30
26
|
effort?: string;
|
|
31
27
|
mcpConfigPath?: string;
|
|
32
28
|
resumeSessionId?: string;
|
|
33
29
|
newSessionId?: string;
|
|
34
30
|
},
|
|
35
|
-
):
|
|
31
|
+
): string[] {
|
|
36
32
|
const args = [
|
|
37
33
|
"-p",
|
|
38
34
|
"--input-format",
|
|
@@ -74,6 +70,28 @@ export function spawnClaude(
|
|
|
74
70
|
args.push("--mcp-config", options.mcpConfigPath);
|
|
75
71
|
}
|
|
76
72
|
|
|
73
|
+
return args;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function spawnClaude(
|
|
77
|
+
modelId: string,
|
|
78
|
+
systemPrompt?: string,
|
|
79
|
+
options?: {
|
|
80
|
+
cwd?: string;
|
|
81
|
+
signal?: AbortSignal;
|
|
82
|
+
effort?: string;
|
|
83
|
+
mcpConfigPath?: string;
|
|
84
|
+
resumeSessionId?: string;
|
|
85
|
+
newSessionId?: string;
|
|
86
|
+
},
|
|
87
|
+
): ChildProcess {
|
|
88
|
+
const args = buildClaudeSpawnArgs(modelId, systemPrompt, {
|
|
89
|
+
effort: options?.effort,
|
|
90
|
+
mcpConfigPath: options?.mcpConfigPath,
|
|
91
|
+
resumeSessionId: options?.resumeSessionId,
|
|
92
|
+
newSessionId: options?.newSessionId,
|
|
93
|
+
});
|
|
94
|
+
|
|
77
95
|
const proc = spawn("claude", args, {
|
|
78
96
|
stdio: ["pipe", "pipe", "pipe"],
|
|
79
97
|
cwd: options?.cwd ?? process.cwd(),
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
forceKillProcess,
|
|
39
39
|
registerProcess,
|
|
40
40
|
cleanupSystemPromptFile,
|
|
41
|
+
buildClaudeSpawnArgs,
|
|
41
42
|
} from "./process-manager.js";
|
|
42
43
|
import { parseLine } from "./stream-parser.js";
|
|
43
44
|
import { createEventBridge } from "./event-bridge.js";
|
|
@@ -61,10 +62,12 @@ import { isPiKnownClaudeTool } from "./tool-mapping.js";
|
|
|
61
62
|
* arrives (e.g. someone embeds pi-claude-cli without a stuck detector).
|
|
62
63
|
*/
|
|
63
64
|
const INACTIVITY_TIMEOUT_MS = 30 * 60_000;
|
|
64
|
-
|
|
65
|
+
function isDebugStreamEnabled(): boolean {
|
|
66
|
+
return process.env.PI_CLAUDE_CLI_DEBUG === "1";
|
|
67
|
+
}
|
|
65
68
|
|
|
66
69
|
function debugLog(message: string): void {
|
|
67
|
-
if (!
|
|
70
|
+
if (!isDebugStreamEnabled()) return;
|
|
68
71
|
console.error(`[pi-claude-cli] ${message}`);
|
|
69
72
|
}
|
|
70
73
|
|
|
@@ -131,19 +134,30 @@ export function streamViaCli(
|
|
|
131
134
|
options?.thinkingBudgets,
|
|
132
135
|
);
|
|
133
136
|
|
|
134
|
-
|
|
135
|
-
proc = spawnClaude(model.id, systemPrompt || undefined, {
|
|
137
|
+
const spawnOptions = {
|
|
136
138
|
cwd,
|
|
137
139
|
signal: options?.signal,
|
|
138
140
|
effort,
|
|
139
141
|
mcpConfigPath: options?.mcpConfigPath,
|
|
140
142
|
resumeSessionId,
|
|
141
143
|
newSessionId: !resumeSessionId ? options?.sessionId : undefined,
|
|
142
|
-
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Spawn subprocess
|
|
147
|
+
proc = spawnClaude(model.id, systemPrompt || undefined, spawnOptions);
|
|
143
148
|
const getStderr = captureStderr(proc);
|
|
144
149
|
|
|
145
150
|
// Register in global process registry for teardown cleanup
|
|
146
151
|
registerProcess(proc);
|
|
152
|
+
const spawnArgs = buildClaudeSpawnArgs(model.id, undefined, {
|
|
153
|
+
effort,
|
|
154
|
+
mcpConfigPath: options?.mcpConfigPath,
|
|
155
|
+
resumeSessionId,
|
|
156
|
+
newSessionId: !resumeSessionId ? options?.sessionId : undefined,
|
|
157
|
+
});
|
|
158
|
+
debugLog(
|
|
159
|
+
`spawned claude subprocess pid=${proc.pid ?? "unknown"} args=${JSON.stringify(spawnArgs)}`,
|
|
160
|
+
);
|
|
147
161
|
|
|
148
162
|
// Write user message to subprocess stdin
|
|
149
163
|
writeUserMessage(proc, prompt);
|
|
@@ -234,10 +248,13 @@ export function streamViaCli(
|
|
|
234
248
|
proc.on("close", (code: number | null, _signal: string | null) => {
|
|
235
249
|
clearTimeout(inactivityTimer);
|
|
236
250
|
if (broken) return; // Break-early kill, expected
|
|
251
|
+
const stderr = getStderr().trim();
|
|
252
|
+
if (stderr) {
|
|
253
|
+
console.warn(`[pi-claude-cli] Claude CLI stderr on close: ${stderr}`);
|
|
254
|
+
}
|
|
237
255
|
if (code !== 0 && code !== null) {
|
|
238
|
-
const stderr = getStderr();
|
|
239
256
|
const message = stderr
|
|
240
|
-
? `Claude CLI exited with code ${code}: ${stderr
|
|
257
|
+
? `Claude CLI exited with code ${code}: ${stderr}`
|
|
241
258
|
: `Claude CLI exited unexpectedly with code ${code}`;
|
|
242
259
|
endStreamWithError(message);
|
|
243
260
|
}
|
|
@@ -324,6 +341,13 @@ export function streamViaCli(
|
|
|
324
341
|
// Guard with streamEnded to avoid pushing done after an error was already pushed.
|
|
325
342
|
if (!streamEnded) {
|
|
326
343
|
const output = bridge.getOutput();
|
|
344
|
+
const contentEvents = output.content || [];
|
|
345
|
+
|
|
346
|
+
if (contentEvents.length === 0) {
|
|
347
|
+
console.warn(
|
|
348
|
+
`[pi-claude-cli] Claude CLI closed without content events (model=${model.id}, sessionId=${options?.sessionId ?? "none"})`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
327
351
|
|
|
328
352
|
// If stopReason is toolUse but there are no pi-known tool calls in content,
|
|
329
353
|
// it means only user MCP tools were called (filtered by event bridge).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runfusion/fusion",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Fusion CLI: HTTP API server, daemon, dashboard launcher, and task tooling for the Fusion AI coding agent.",
|
|
6
6
|
"homepage": "https://github.com/Runfusion/Fusion#readme",
|
|
@@ -75,11 +75,7 @@
|
|
|
75
75
|
"typebox": "^1.0.0",
|
|
76
76
|
"typescript": "^5.7.0",
|
|
77
77
|
"vitest": "^3.1.0",
|
|
78
|
-
"yaml": "^2.8.3"
|
|
79
|
-
"@fusion/core": "0.7.1",
|
|
80
|
-
"@fusion/pi-claude-cli": "0.7.1",
|
|
81
|
-
"@fusion/engine": "0.7.1",
|
|
82
|
-
"@fusion/dashboard": "0.7.1"
|
|
78
|
+
"yaml": "^2.8.3"
|
|
83
79
|
},
|
|
84
80
|
"repository": {
|
|
85
81
|
"type": "git",
|
|
@@ -29,6 +29,7 @@ Update fields on an existing task. Supports modifying the title, description, de
|
|
|
29
29
|
| `description` | string | — | New task description |
|
|
30
30
|
| `depends` | array | — | New dependency list — replaces existing dependencies (e.g. ['FN-001', 'FN-002']) |
|
|
31
31
|
| `agentId` | union | — | Agent ID to assign this task to, or null to clear (e.g. 'agent-abc123') |
|
|
32
|
+
| `nodeId` | union | — | Node ID override for this task, or null to clear |
|
|
32
33
|
|
|
33
34
|
### fn_task_list
|
|
34
35
|
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import{r,j as t}from"./vendor-react-K0fH_qHe.js";import{c as Ze,bp as De,bq as de,br as et,bs as ue,s as Me,bt as ge,bu as we,bv as xe,bw as ye,bx as tt,by as ae,bz as rt,bA as st,bB as nt,bC as at,bD as it,bE as ie,L as ee,S as lt,bF as ct,bG as ot,N as dt,bH as Ue,T as Ae,bI as ut,J as vt,aM as mt,a5 as ft,aj as pt,R as ht}from"./index-DJDWSrju.js";import"./vendor-xterm-DzcZoU0P.js";/**
|
|
2
|
-
* @license lucide-react v1.7.0 - ISC
|
|
3
|
-
*
|
|
4
|
-
* This source code is licensed under the ISC license.
|
|
5
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
6
|
-
*/const bt=[["path",{d:"M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8",key:"1p45f6"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}]],gt=Ze("rotate-cw",bt),ve=500,wt=3e3;function B(e){return e instanceof Error?e.message:String(e)}function Pe(e){return e.length<=ve?e:e.slice(-ve)}function le(e,i){return Pe([...e,i])}function G(e){try{return JSON.parse(e)}catch{return null}}function Y(e){const i=e.text??"";return e.stream==="stderr"?`[stderr] ${i}`:i}function xt(e){return e?.previewUrl??null}async function Ne(e){return rt(e)}async function Se(e,i){return st(e,i)}async function yt(e){return nt(e)}async function Nt(e){return at(e)}async function je(e){return it(e)}function V(e){try{return e()}catch{return null}}function ce(){return typeof V(()=>de)=="function"&&typeof V(()=>De)=="function"}function I(e){return{config:{id:e.id??"default",name:e.name??"Dev Server",command:e.command??"",cwd:e.cwd??"."},status:e.status,runtime:e.pid?{pid:e.pid,startedAt:e.startedAt??new Date().toISOString(),exitCode:e.exitCode??void 0,previewUrl:e.previewUrl}:void 0,previewUrl:e.previewUrl??e.detectedUrl??e.manualUrl??void 0,logHistory:(e.logs??[]).map(i=>({timestamp:new Date().toISOString(),stream:i.startsWith("[stderr]")?"stderr":"stdout",text:i.replace(/^\[stderr\]\s*/,"")}))}}function St(e){const[i,o]=r.useState(null),[b,y]=r.useState([]),[w,f]=r.useState([]),[k,M]=r.useState([]),[C,P]=r.useState(!0),[N,u]=r.useState(null),n=r.useRef(0),[d,U]=r.useState(null),R=r.useCallback(a=>{if(o(a),a?.logHistory){const s=a.logHistory.slice(-ve).map(Y);f(s)}},[]),A=r.useCallback(async()=>{const a=n.current;try{if(ce())if(d){const s=await De(d,e);if(n.current!==a)return;R(s)}else{const s=await de(e);if(n.current!==a)return;y(s),s.length>0&&(U(s[0].config.id),R(s[0]))}else{const s=await Ne(e);if(n.current!==a)return;const c=I(s);y([c]),R(c)}u(null)}catch(s){if(n.current!==a)return;u(B(s))}},[R,e,d]);r.useEffect(()=>{n.current+=1;const a=n.current;o(null),y([]),f([]),M([]),P(!0),u(null),U(null),(async()=>{try{const[c,l]=await Promise.allSettled([ce()?de(e):Ne(e).then(S=>[I(S)]),typeof V(()=>ae)=="function"?ae(e):je(e).then(S=>S.map(g=>({name:g.name,command:g.command,cwd:g.cwd,scriptName:g.scriptName,packagePath:g.packagePath})))]);if(n.current!==a)return;let v=null;if(c.status==="fulfilled"){const S=c.value;if(y(S),S.length>0){const g=S[0];ce()&&U(g.config.id),R(g)}}else v=B(c.reason);l.status==="fulfilled"&&M(l.value),v&&u(v)}catch(c){if(n.current!==a)return;u(B(c))}finally{n.current===a&&P(!1)}})()},[R,e]),r.useEffect(()=>{const a=d?et(d,e):typeof V(()=>ue)=="function"?ue(e):null;if(!a)return;const s=n.current,c=Me(a,{events:{history:l=>{if(n.current!==s)return;const v=G(l.data);if(v?.lines){const S=v.lines.map(Y);f(Pe(S))}},log:l=>{if(n.current!==s)return;const v=G(l.data);if(v){const S=typeof v.line=="string"?v.line:Y(v);f(g=>le(g,S))}},"dev-server:log":l=>{if(n.current!==s)return;const v=G(l.data);if(v){const S=typeof v.line=="string"?v.line:Y(v);f(g=>le(g,S))}},"dev-server:output":l=>{if(n.current!==s)return;const v=G(l.data);v?.line&&f(S=>le(S,v.line))},status:l=>{if(n.current!==s)return;const v=G(l.data),S=v?.status;S&&o(g=>g&&{...g,status:S,runtime:v.pid?{...g.runtime??{startedAt:new Date().toISOString()},pid:v.pid}:g.runtime})},"dev-server:status":l=>{if(n.current!==s)return;const v=G(l.data);if(v?.status){const S=I(v);o(S),y([S])}},stopped:()=>{n.current},failed:()=>{n.current}},onReconnect:()=>{n.current===s&&A()},onError:()=>{n.current===s&&u(l=>l??"Lost log stream connection.")}});return()=>{c()}},[e,A,d]),r.useEffect(()=>{if(i?.status!=="running"&&i?.status!=="starting")return;const a=setInterval(()=>{A()},wt);return()=>{clearInterval(a)}},[A,i?.status]);const _=r.useCallback(async(a,s)=>{n.current+=1;const c=n.current;try{let l;if(d&&typeof V(()=>ge)=="function")l=await ge(d,e);else{const v=await Se({command:a,cwd:s},e);l=I(v)}if(n.current!==c)return;o(l),u(null)}catch(l){if(n.current!==c)return;throw u(B(l)),l}},[e,d]),D=r.useCallback(async()=>{n.current+=1;const a=n.current;try{let s;if(d&&typeof V(()=>we)=="function")s=await we(d,e);else{const c=await yt(e);s=I(c)}if(n.current!==a)return;o(s),u(null)}catch(s){if(n.current!==a)return;throw u(B(s)),s}},[e,d]),j=r.useCallback(async()=>{n.current+=1;const a=n.current;try{let s;if(d&&typeof V(()=>xe)=="function")s=await xe(d,e);else{const c=await Nt(e);s=I(c)}if(n.current!==a)return;o(s),u(null)}catch(s){if(n.current!==a)return;throw u(B(s)),s}},[e,d]),T=r.useCallback(async a=>{n.current+=1;const s=n.current;try{let c;if(d&&typeof V(()=>ye)=="function")c=await ye(d,a,e);else{const l=await tt({url:a},e);c={url:l.manualUrl??l.previewUrl??l.detectedUrl??null,source:l.manualUrl?"manual":"auto"}}if(n.current!==s)return;o(l=>l?{...l,previewUrl:c.url??void 0}:null),u(null)}catch(c){if(n.current!==s)return;throw u(B(c)),c}},[e,d]),h=r.useCallback(async()=>{n.current+=1;const a=n.current;try{let s;try{s=await ae(e)}catch{s=(await je(e)).map(l=>({name:l.name,command:l.command,cwd:l.cwd,scriptName:l.scriptName,packagePath:l.packagePath}))}if(n.current!==a)return;M(s),u(null)}catch(s){if(n.current!==a)return;throw u(B(s)),s}},[e]),p=r.useCallback(async(a,s)=>{if(typeof a!="string"&&!d)try{const v=a,S=await Se({command:v.command,cwd:v.cwd,scriptName:v.scriptName,packagePath:v.packagePath??v.cwd},e),g=I(S);o(g),y([g]),u(null);return}catch(v){throw u(B(v)),v}const c=typeof a=="string"?a:a.command,l=typeof a=="string"?s:a.cwd;await _(c,l)},[e,_,d]),E=r.useCallback(async()=>{await D()},[D]),L=r.useCallback(async()=>{await j()},[j]),z=r.useCallback(async a=>{await T(a)},[T]),$=r.useCallback(async()=>{await h()},[h]),O=r.useCallback(async()=>{await A()},[A]),H=xt(i),x=i?{...i,pid:i.runtime?.pid}:null;return{session:i,sessions:b,logs:w,detectedCommands:k,previewUrl:H,isLoading:C,error:N,startServer:_,stopServer:D,restartServer:j,setPreviewUrl:T,detectCommands:h,refresh:A,candidates:k,serverState:x,loading:C,start:p,stop:E,restart:L,setManualUrl:z,detect:$,refreshStatus:O}}const ke=500,Ce=100;function Te(e){return e.length>ke?e.slice(-ke):e}function ze(e){return e==="stderr"?"stderr":"stdout"}function K(e){try{return JSON.parse(e.data)}catch{return null}}function Z(e,i){return{id:typeof e.id=="number"&&Number.isFinite(e.id)?e.id:i,text:typeof e.text=="string"?e.text:"",stream:ze(e.stream),timestamp:typeof e.timestamp=="string"?e.timestamp:""}}function jt(e,i){return{id:i,text:e,stream:"stdout",timestamp:""}}function oe(e,i){if(i.length===0)return e;const o=[...e],b=new Set(e.map(y=>y.id));for(const y of i)b.has(y.id)||(b.add(y.id),o.push(y));return o.sort((y,w)=>y.id-w.id),Te(o)}function kt(e,i){const[o,b]=r.useState([]),[y,w]=r.useState(!1),[f,k]=r.useState(!1),[M,C]=r.useState(!1),[P,N]=r.useState(null),u=r.useRef(null),n=r.useRef(!1),d=r.useRef(0),U=r.useRef(0),R=r.useRef(e),A=r.useRef(i),_=r.useRef(0),D=r.useRef(1);(R.current!==e||A.current!==i)&&(R.current=e,A.current=i,d.current++,n.current=!0,_.current=0,D.current=1,b([]),w(!1),k(!1),C(!1),N(null),u.current&&(u.current(),u.current=null)),r.useEffect(()=>{if(!i){u.current&&(u.current(),u.current=null);return}const E=d.current,L=++U.current;n.current=!1,w(!0);const z=(x,a)=>{const s=Te(x);b(s),N(a),C(a!==null?a>s.length:!1);const c=s.length>0?s[s.length-1].id:0;_.current=c,D.current=c+1},$=x=>{if(n.current||d.current!==E||!x||typeof x!="object")return;const a=x.lines;if(!Array.isArray(a))return;if(a.length>0&&typeof a[0]=="string"){const c=a.filter(l=>typeof l=="string").map((l,v)=>jt(l,v+1));z(c,c.length);return}const s=a.filter(c=>!!c&&typeof c=="object").map((c,l)=>Z(c,l+1));z(s,s.length)},O=x=>{if(n.current||d.current!==E)return;const a=typeof x.text=="string"?x.text:typeof x.line=="string"?x.line:null;if(!a)return;const s=D.current,c=typeof x.id=="number"&&Number.isFinite(x.id)?x.id:s,l={id:c,text:a,stream:ze(x.stream),timestamp:typeof x.timestamp=="string"?x.timestamp:""};_.current=Math.max(_.current,c),D.current=Math.max(D.current,c+1),b(v=>oe(v,[l])),N(v=>v===null?v:Math.max(v+1,l.id))};async function H(){try{const a=await ie({maxLines:Ce},e);if(n.current||d.current!==E||U.current!==L)return;const s=a.lines.map((c,l)=>Z(c,l+1));z(s,a.totalLines)}catch{if(n.current||d.current!==E||U.current!==L)return;z([],null)}finally{!n.current&&d.current===E&&U.current===L&&w(!1)}const x=ue(e);u.current=Me(x,{events:{"dev-server:log":a=>{const s=K(a);s&&O(s)},log:a=>{const s=K(a);s&&O(s)},history:a=>{const s=K(a);$(s)},"dev-server:history":a=>{const s=K(a);$(s)}},onReconnect:()=>{n.current||d.current!==E||ie({lastEventId:_.current,maxLines:50},e).then(a=>{if(n.current||d.current!==E)return;const s=a.lines.map((c,l)=>Z(c,D.current+l));if(s.length>0){const c=s[s.length-1].id;_.current=Math.max(_.current,c),D.current=Math.max(D.current,c+1)}b(c=>oe(c,s)),N(a.totalLines)}).catch(()=>{})}})}return H(),()=>{n.current=!0,u.current&&(u.current(),u.current=null)}},[i,e]);const T=r.useCallback(async()=>{if(!i||f)return;const E=d.current,L=o.length;k(!0);try{const z=await ie({maxLines:Ce,offset:L},e);if(n.current||d.current!==E)return;const $=z.lines.map((O,H)=>Z(O,H+1));b(O=>oe($,O)),C(z.totalLines>L+$.length),N(z.totalLines)}catch{}finally{k(!1)}},[i,o.length,f,e]),h=r.useCallback(()=>{b([])},[]),p=u.current!==null&&!y&&!n.current;return{entries:o,loading:y,loadingMore:f,hasMore:M,total:P,loadMore:T,clear:h,logs:o,isStreaming:p,clearLogs:h}}const Ct="This preview appears to block iframe embedding. Open it in a new tab instead.",Et="The preview URL could not be loaded. Verify the server is running and the URL is correct.",Lt="Preview is taking longer than expected and may block iframe embedding.";function Rt(e){return e==="blocked"?Ct:e==="error"?Et:null}function _t(e,i={}){const{loadTimeoutMs:o=1e4,detectionMethod:b=null}=i,y=r.useRef(null),w=r.useRef(null),[f,k]=r.useState("unknown"),[M,C]=r.useState(null),[P]=r.useState(b),N=r.useCallback(()=>{w.current!==null&&(window.clearTimeout(w.current),w.current=null)},[]),u=r.useCallback(j=>{k(j),C(Rt(j))},[]),n=r.useCallback(()=>{k("blocked"),C(Lt)},[]);r.useEffect(()=>{if(N(),!e){k("unknown"),C(null);return}k("unknown"),C(null);let j=!1;return queueMicrotask(()=>{j||(k("loading"),C(null))}),()=>{j=!0,N()}},[N,e]),r.useEffect(()=>{if(f!=="loading"){N();return}const j=setTimeout(()=>{w.current=null,n()},o);return w.current=j,()=>{clearTimeout(j),w.current===j&&(w.current=null)}},[N,f,o,n]);const d=r.useCallback(()=>{const j=y.current;if(!j){u("embedded");return}try{if(j.contentWindow?.location?.href==="about:blank"&&j.src!=="about:blank"){u("blocked");return}}catch{}u("embedded")},[u]),U=r.useCallback(()=>{u("error")},[u]);r.useEffect(()=>{if(N(),!e){k("unknown"),C(null);return}k("unknown"),C(null)},[N]);const R=r.useCallback(()=>{N(),k("unknown"),C(null)},[N]),A=R,_=r.useMemo(()=>f==="embedded",[f]),D=r.useMemo(()=>f==="blocked"||f==="error",[f]);return{embedStatus:f,isEmbedded:_,isBlocked:D,blockReason:M,detectionMethod:P,iframeRef:y,resetEmbedStatus:R,setEmbedStatus:u,retry:A,embedContext:M,handleIframeLoad:d,handleIframeError:U}}const Dt=/\x1b\[[0-9;]*m/g;function me(e){return e.replace(Dt,"")}function Mt(e){if(!e)return"";const i=new Date(e);return Number.isNaN(i.getTime())?"":i.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1})}function Ut(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function At(e){if(e.stream==="stderr")return"error";const i=me(e.text).toLowerCase();return/\b(warn|warning)\b/.test(i)?"warn":/\b(error|fatal)\b/.test(i)?"error":"info"}function Pt(e,i){if(!i)return e;const o=new RegExp(`(${Ut(i)})`,"ig"),b=e.split(o),y=i.toLowerCase();return t.jsx(t.Fragment,{children:b.map((w,f)=>w.toLowerCase()===y?t.jsx("mark",{children:w},`${w}-${f}`):t.jsx("span",{children:w},`${w}-${f}`))})}function Tt({entries:e,loading:i,loadingMore:o,hasMore:b,total:y,onLoadMore:w,isRunning:f}){const k=r.useRef(null),M=r.useRef(e.length),C=r.useRef(f),[P,N]=r.useState(!1),[u,n]=r.useState(!1),[d,U]=r.useState(""),[R,A]=r.useState("all"),_=r.useMemo(()=>R==="all"?e:e.filter(p=>At(p)===R),[e,R]),D=r.useMemo(()=>{const p=d.trim().toLowerCase();return p?_.filter(E=>me(E.text).toLowerCase().includes(p)):_},[_,d]),j=D.length,T=r.useCallback(()=>{const p=k.current;p&&(p.scrollTop=p.scrollHeight,n(!1))},[]);r.useEffect(()=>{const p=C.current,E=M.current,L=e.length>E;f&&(!p||!u&&L)&&T(),C.current=f,M.current=e.length},[e.length,f,u,T]);const h=r.useCallback(()=>{const p=k.current;if(!p)return;const L=p.scrollTop+p.clientHeight>=p.scrollHeight-50;n(!L)},[]);return r.useEffect(()=>{i||e.length===0||!u&&f&&T()},[e,f,u,i,T]),i&&e.length===0?t.jsx("section",{className:"devserver-log-viewer","data-testid":"devserver-log-viewer",children:t.jsxs("div",{className:"devserver-log-viewer__loading","data-testid":"devserver-log-loading",children:[t.jsx(ee,{size:16,className:"devserver-log-viewer__spinner"}),t.jsx("span",{children:"Loading logs…"})]})}):t.jsxs("section",{className:`devserver-log-viewer${P?" devserver-log-viewer--fullscreen":""}`,"data-testid":"devserver-log-viewer",children:[t.jsxs("header",{className:"devserver-log-viewer__toolbar",children:[t.jsxs("div",{className:"devserver-log-viewer__toolbar-meta",children:[t.jsx("span",{className:"devserver-log-viewer__title",children:"Logs"}),t.jsxs("span",{className:"devserver-log-viewer__count","data-testid":"devserver-log-count",children:[y!==null?`${e.length}/${y}`:`${e.length}`," lines"]})]}),t.jsxs("div",{className:"devserver-log-viewer__toolbar-actions",children:[t.jsxs("label",{className:"devserver-log-viewer__severity",htmlFor:"devserver-log-severity-filter",children:[t.jsx("span",{className:"visually-hidden",children:"Filter logs by severity"}),t.jsxs("select",{id:"devserver-log-severity-filter",className:"select devserver-log-viewer__severity-select",value:R,onChange:p=>A(p.target.value),"data-testid":"devserver-log-severity-filter","aria-label":"Filter logs by severity",children:[t.jsx("option",{value:"all",children:"All severities"}),t.jsx("option",{value:"info",children:"Info"}),t.jsx("option",{value:"warn",children:"Warn"}),t.jsx("option",{value:"error",children:"Error"})]})]}),t.jsxs("label",{className:"devserver-log-viewer__search",htmlFor:"devserver-log-search",children:[t.jsx("span",{className:"visually-hidden",children:"Search logs"}),t.jsx(lt,{size:14}),t.jsx("input",{id:"devserver-log-search",className:"input devserver-log-viewer__search-input",type:"text",value:d,onChange:p=>U(p.target.value),placeholder:"Search logs","data-testid":"devserver-log-search-input","aria-label":"Search logs"})]}),d.trim().length>0&&t.jsxs("span",{className:"devserver-log-viewer__matches","data-testid":"devserver-log-match-count",children:[j," match",j===1?"":"es"]}),t.jsx("button",{type:"button",className:"btn btn-sm btn-icon",onClick:()=>N(p=>!p),"data-testid":"devserver-log-fullscreen-toggle","aria-label":P?"Exit fullscreen logs":"Enter fullscreen logs",children:P?t.jsx(ct,{size:14}):t.jsx(ot,{size:14})})]})]}),t.jsxs("div",{className:"devserver-log-viewer__body",children:[b&&t.jsx("div",{className:"devserver-log-viewer__load-more","data-testid":"devserver-log-load-more",children:t.jsx("button",{type:"button",className:"btn btn-sm touch-target",onClick:w,disabled:o,"data-testid":"devserver-log-load-more-button",children:o?t.jsxs(t.Fragment,{children:[t.jsx(ee,{size:14,className:"devserver-log-viewer__spinner"}),"Loading older logs…"]}):"Load older logs"})}),t.jsxs("div",{ref:k,className:"devserver-log-viewer__content",onScroll:h,"data-testid":"devserver-log-content",children:[!i&&D.length===0&&t.jsx("p",{className:"devserver-log-viewer__empty","data-testid":"devserver-log-empty",children:e.length===0?"No logs yet. Start the dev server to see output.":_.length===0?"No log lines match the selected severity.":"No log lines match your search."}),D.map(p=>{const E=me(p.text),L=Mt(p.timestamp);return t.jsxs("div",{className:"devserver-log-line",children:[L&&t.jsx("span",{className:"devserver-log-timestamp","data-testid":"devserver-log-timestamp",children:L}),p.stream==="stderr"&&t.jsx("span",{className:"devserver-log-stream-badge","data-testid":"devserver-log-stderr-badge",children:"ERR"}),t.jsx("span",{className:"devserver-log-text",children:Pt(E,d.trim())})]},p.id)})]}),u&&f&&t.jsxs("button",{type:"button",className:"btn btn-sm devserver-log-viewer__new-logs-button",onClick:T,"data-testid":"devserver-log-jump-button",children:[t.jsx(dt,{size:14}),"New logs"]})]})]})}const zt="devserver-preview-iframe";function Ft({url:e,embedStatus:i,onEmbedStatusChange:o,iframeRef:b,blockReason:y,onRetry:w,className:f=zt,embedContext:k}){const M=y??k??null,[C,P]=r.useState(0);r.useEffect(()=>{!e||i!=="unknown"||(P(d=>d+1),o("loading"))},[i,o,e]);const N=r.useCallback(()=>{const d=b.current;if(!d){o("embedded");return}try{if(d.contentWindow?.location?.href==="about:blank"&&d.src!=="about:blank"){o("blocked");return}}catch{}o("embedded")},[b,o]),u=r.useCallback(d=>{d.stopPropagation(),o("error")},[o]),n=r.useCallback(()=>{e&&window.open(e,"_blank","noopener,noreferrer")},[e]);return e?t.jsxs("div",{className:"devserver-preview-iframe-shell",children:[t.jsx("iframe",{src:e,ref:b,sandbox:"allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox",className:f,title:"Dev server preview",onLoad:N,onError:u,onErrorCapture:u,"data-testid":"devserver-preview-iframe"},`${e}-${C}`),i==="loading"&&t.jsxs("div",{className:"devserver-preview-overlay","data-testid":"devserver-preview-loading",children:[t.jsx(ee,{size:16,className:"dev-server-spin"}),t.jsx("span",{children:"Loading preview..."})]}),i==="blocked"&&t.jsxs("div",{className:"devserver-preview-blocked-panel",role:"alert","data-testid":"devserver-preview-blocked-panel",children:[t.jsx(Ue,{className:"devserver-preview-blocked-icon","aria-hidden":"true"}),t.jsxs("div",{children:[t.jsx("p",{className:"devserver-preview-blocked-title",children:"Preview cannot be embedded"}),M&&t.jsx("p",{className:"devserver-preview-blocked-context",children:M})]}),t.jsx("p",{className:"devserver-preview-blocked-description",children:"You can view the preview in a separate browser tab."}),t.jsxs("div",{className:"devserver-preview-blocked-actions",children:[t.jsx("button",{type:"button",className:"btn btn-primary",onClick:n,children:"Open in new tab"}),w&&t.jsx("button",{type:"button",className:"btn btn-sm",onClick:w,children:"Retry"})]})]}),i==="error"&&t.jsxs("div",{className:"devserver-preview-error-panel",role:"alert","data-testid":"devserver-preview-error-panel",children:[t.jsx(Ae,{className:"devserver-preview-blocked-icon","aria-hidden":"true"}),t.jsxs("div",{children:[t.jsx("p",{className:"devserver-preview-blocked-title",children:"Unable to load preview"}),M&&t.jsx("p",{className:"devserver-preview-blocked-context",children:M})]}),t.jsx("p",{className:"devserver-preview-blocked-description",children:"You can view the preview in a separate browser tab."}),t.jsxs("div",{className:"devserver-preview-blocked-actions",children:[t.jsx("button",{type:"button",className:"btn btn-primary",onClick:n,children:"Open in new tab"}),w&&t.jsx("button",{type:"button",className:"btn btn-sm",onClick:w,children:"Retry"})]})]})]}):null}const Ee={stopped:{className:"dev-server-status-badge--stopped",label:"Stopped"},starting:{className:"dev-server-status-badge--starting",label:"Starting..."},running:{className:"dev-server-status-badge--running",label:"Running"},stopping:{className:"dev-server-status-badge--starting",label:"Stopping..."},failed:{className:"dev-server-status-badge--failed",label:"Failed"}};function Le(e){return e instanceof Error?e.message:String(e)}function Fe(e){return e==="."?"root":e}function Re(e){return e?e==="root"?".":e:null}function _e(e,i,o){return!i||e.scriptName!==i?!1:o?Fe(e.cwd)===o:!0}function $t(e){return e.cwd==="."?"root":e.cwd}function Ot(e){return e.length<=60?e:`${e.slice(0,60)}…`}function qt({addToast:e,projectId:i}){const{session:o,detectedCommands:b,previewUrl:y,isLoading:w,error:f,startServer:k,stopServer:M,restartServer:C,setPreviewUrl:P,detectCommands:N,refresh:u}=St(i),n=o?.status??"stopped",d=n==="running"||n==="starting",U=Ee[n]??Ee.stopped,{entries:R,loading:A,loadingMore:_,hasMore:D,total:j,loadMore:T}=kt(i,!!i),h=y,p=o?.config?.cwd??null,[E,L]=r.useState(!0),[z,$]=r.useState(""),[O,H]=r.useState(""),[x,a]=r.useState(null),[s,c]=r.useState(null),[l,v]=r.useState("embedded"),S=l==="embedded"?h:null,{embedStatus:g,setEmbedStatus:$e,resetEmbedStatus:X,iframeRef:W,isEmbedded:Oe,isBlocked:te,blockReason:re,retry:fe}=_t(S),[pe,q]=r.useState(!1),he=r.useRef(g);r.useEffect(()=>{const m=he.current!==g;te&&m&&q(!0),g==="embedded"&&q(!1),he.current=g},[g,te]),r.useEffect(()=>{q(!1)},[h]);const J=r.useMemo(()=>{if(!x)return null;const m=Re(p);return b.find(F=>!(F.scriptName!==x||m&&F.cwd!==m||o?.config?.command&&F.command!==o.config.command))??b.find(F=>_e(F,x,p))??null},[b,o?.config?.command,x,p]);r.useEffect(()=>{typeof N=="function"&&N().catch(m=>{e(Le(m),"error")})},[e,N]),r.useEffect(()=>{if(x){L(!1);return}L(!0)},[x]),r.useEffect(()=>{if(o?.status==="running"||o?.status==="starting"){o.config?.command?.trim().length>0&&$(o.config.command);return}if(J){$(J.command);return}b.length>0&&$(m=>m.trim().length>0?m:b[0]?.command??"")},[b,J,o?.config?.command,o?.status]),r.useEffect(()=>{H(h??"")},[h]);const se=r.useCallback(()=>{h&&window.open(h,"_blank","noopener,noreferrer")},[h]),be=r.useCallback(()=>{q(!1),fe()},[fe]),Be=r.useCallback(()=>{try{const m=W.current;if(m?.contentWindow){m.contentWindow.location.reload(),q(!1),X();return}}catch{}if(!(!h||!W.current))try{const m=new URL(h);m.searchParams.set("_t",Date.now().toString()),W.current.src=m.toString(),q(!1),X()}catch{W.current.src=h,q(!1),X()}},[h,W,X]),Q=r.useCallback(async(m,F,ne)=>{c(m);try{await F(),e(ne,"success")}catch(Ke){e(Le(Ke),"error")}finally{c(null)}},[e]),He=r.useCallback(m=>{a(m.scriptName),L(!1),$(m.command),e(`Selected ${m.scriptName} script.`,"success")},[e]),Ve=r.useCallback(()=>{a(null),L(!0),e("Cleared selected dev server script.","success")},[e]),qe=()=>{const m=z.trim();if(m.length===0){e("Enter a command before starting the dev server.","warning");return}const F=Re(p)??".",ne=J?.cwd??F;Q("start",()=>k(m,ne),"Dev server started.")},Ie=()=>{Q("stop",M,"Dev server stopped.")},We=()=>{Q("restart",C,"Dev server restarted.")},Ge=()=>{const m=O.trim(),F=m.length>0?m:null;Q("preview",()=>P(F),F?"Preview URL updated.":"Preview URL override cleared.")},Xe=r.useCallback(()=>{f&&u()},[f,u]),Je=n==="starting"||n==="running"||s!==null,Qe=n==="stopped"||s!==null,Ye=n==="stopped"||n==="starting"||s!==null;return t.jsxs("div",{className:"dev-server-view","data-testid":"dev-server-view",children:[t.jsxs("section",{className:"dev-server-header","aria-label":"Dev server controls header",children:[t.jsxs("div",{className:"dev-server-header-title",children:[t.jsx(ut,{size:16}),t.jsx("h2",{children:"Dev Server"}),t.jsx("span",{className:`dev-server-status-badge ${U.className}`,"data-testid":"dev-server-status-badge",children:U.label})]}),t.jsxs("div",{className:"dev-server-header-actions",children:[t.jsxs("button",{type:"button",className:"btn btn-primary btn-sm",onClick:qe,disabled:Je,"data-testid":"dev-server-start-button",children:[t.jsx(vt,{size:14}),t.jsx("span",{children:s==="start"?"Starting...":"Start"})]}),t.jsxs("button",{type:"button",className:"btn btn-danger btn-sm",onClick:Ie,disabled:Qe,"data-testid":"dev-server-stop-button",children:[t.jsx(mt,{size:14}),t.jsx("span",{children:s==="stop"?"Stopping...":"Stop"})]}),t.jsxs("button",{type:"button",className:"btn btn-sm",onClick:We,disabled:Ye,"data-testid":"dev-server-restart-button",children:[t.jsx(gt,{size:14}),t.jsx("span",{children:s==="restart"?"Restarting...":"Restart"})]})]})]}),t.jsxs("section",{className:"dev-server-panel dev-server-config","aria-label":"Dev server configuration",children:[t.jsxs("div",{className:"dev-server-section-header",children:[t.jsx("h3",{children:"Configuration"}),w&&t.jsx("span",{className:"dev-server-muted",children:"Loading..."})]}),w&&!o&&b.length===0&&t.jsxs("div",{className:"dev-server-loading-state","data-testid":"dev-server-loading-state",children:[t.jsx(ee,{size:16,className:"dev-server-spin"}),t.jsx("span",{children:"Loading dev server configuration..."})]}),f&&t.jsxs("div",{className:"dev-server-error-box",role:"alert","data-testid":"dev-server-error-box",children:[t.jsx("p",{children:f}),t.jsx("button",{type:"button",className:"btn btn-sm",onClick:Xe,children:"Retry"})]}),t.jsxs("div",{className:"dev-server-section",children:[t.jsx("h3",{children:"Script Selection"}),x&&t.jsxs("div",{className:"dev-server-selected","data-testid":"dev-server-selected-summary",children:[t.jsx("span",{className:"dev-server-candidate-name",children:x}),t.jsx("span",{className:"dev-server-candidate-source",children:p??"root"}),t.jsx("button",{type:"button",className:"btn btn-sm",onClick:()=>L(!0),"data-testid":"dev-server-change-selection",children:"Change"}),t.jsx("button",{type:"button",className:"btn btn-danger btn-sm",onClick:Ve,"data-testid":"dev-server-clear-selection",children:"Clear"})]}),E&&b.length===0&&t.jsxs("p",{className:"dev-server-empty-state","data-testid":"dev-server-empty-candidates",children:["No dev server scripts detected. Check that your project has a ",t.jsx("code",{children:"package.json"})," with a ",t.jsx("code",{children:"dev"}),", ",t.jsx("code",{children:"start"}),", or similar script."]}),E&&b.length>0&&t.jsx("div",{className:"dev-server-candidates","data-testid":"dev-server-candidates",children:b.map(m=>{const F=_e(m,x,p);return t.jsxs("button",{type:"button",className:`dev-server-candidate ${F?"dev-server-candidate--selected":""}`,onClick:()=>He(m),"data-testid":`dev-server-candidate-${m.scriptName}-${Fe(m.cwd)}`,children:[t.jsx("span",{className:"dev-server-candidate-name",children:m.scriptName}),t.jsx("span",{className:"dev-server-candidate-command",children:Ot(m.command)}),t.jsx("span",{className:"dev-server-candidate-source",children:$t(m)})]},`${m.cwd}::${m.scriptName}::${m.command}`)})})]}),t.jsxs("div",{className:"dev-server-field-group",children:[t.jsx("label",{htmlFor:"dev-server-command",className:"dev-server-label",children:"Command"}),t.jsx("input",{id:"dev-server-command",className:"input",value:z,onChange:m=>$(m.target.value),placeholder:"pnpm dev","data-testid":"dev-server-command-input",readOnly:n==="running"||n==="starting"})]}),(n==="running"||n==="starting")&&o&&t.jsxs("div",{className:"dev-server-current-command","data-testid":"dev-server-current-command",children:[t.jsx("span",{className:"dev-server-label",children:"Running command"}),t.jsx("code",{children:o.config?.command??z})]}),t.jsxs("div",{className:"dev-server-preview-override",children:[t.jsx("label",{htmlFor:"dev-server-preview-input",className:"dev-server-label",children:"Preview URL Override"}),t.jsx("input",{id:"dev-server-preview-input",className:"input",type:"url",value:O,onChange:m=>H(m.target.value),placeholder:"http://localhost:3000","data-testid":"dev-server-preview-input"}),t.jsx("button",{type:"button",className:"btn btn-primary btn-sm",onClick:Ge,disabled:s==="preview","data-testid":"dev-server-set-preview",children:"Save"})]}),h&&t.jsxs("p",{className:"dev-server-preview-hint",children:["Auto-detected: ",h]})]}),t.jsx("div",{className:"dev-server-content",children:t.jsxs("section",{className:"dev-server-panel dev-server-logs-panel","data-testid":"dev-server-logs-panel","aria-label":"Dev server logs",children:[t.jsxs("div",{className:"dev-server-section-header",children:[t.jsx("h3",{children:"Logs"}),t.jsxs("span",{className:"dev-server-muted",children:[j??R.length," lines"]})]}),t.jsx("div",{className:"dev-server-logs-viewer","data-testid":"dev-server-log-viewer",children:t.jsx(Tt,{entries:R,loading:A,loadingMore:_,hasMore:D,total:j,onLoadMore:T,isRunning:d})})]})}),t.jsxs("section",{className:"dev-server-panel devserver-preview-panel","data-testid":"devserver-preview-panel","aria-label":"Dev server preview",children:[t.jsxs("div",{className:"devserver-preview-header",children:[t.jsxs("div",{className:"devserver-preview-title",children:[t.jsx(ft,{size:14}),t.jsx("span",{children:"Preview"})]}),t.jsxs("span",{className:"devserver-preview-url-badge devserver-preview-url-badge--auto",title:h??"No preview URL","data-testid":"devserver-preview-url-badge",children:["Auto",h?` · ${h}`:" · Not available"]}),t.jsxs("div",{className:"devserver-preview-actions",children:[t.jsx("button",{type:"button",className:"btn btn-sm",onClick:()=>v(m=>m==="embedded"?"external":"embedded"),"data-testid":"devserver-preview-mode-toggle",children:l==="embedded"?"External only":"Embedded"}),t.jsx("button",{type:"button",className:"btn btn-sm btn-icon",title:"Open in new tab",onClick:se,disabled:!h,"data-testid":"devserver-preview-open-tab",children:t.jsx(pt,{size:14})}),t.jsx("button",{type:"button",className:"btn btn-sm btn-icon",title:"Refresh preview",onClick:Be,disabled:!h,"data-testid":"devserver-preview-refresh",children:t.jsx(ht,{size:14})})]})]}),t.jsxs("div",{className:"devserver-preview-container","data-embed-status":g,"data-embedded":Oe?"true":"false",children:[!h&&!d&&t.jsx("p",{className:"devserver-preview-empty",children:"Start a dev server to see a live preview here."}),!h&&d&&t.jsx("p",{className:"devserver-preview-empty",children:"No preview URL detected. Start the dev server or set a manual URL to preview your app."}),h&&l==="external"&&t.jsxs("div",{className:"devserver-preview-external-only","data-testid":"devserver-preview-external-only",children:[t.jsx("p",{children:"Embedded preview is disabled. Open your app in a separate browser tab."}),t.jsx("button",{type:"button",className:"btn btn-primary btn-sm touch-target",onClick:se,"data-testid":"devserver-preview-external-open-tab",children:"Open in new tab"})]}),h&&l==="embedded"&&pe&&te&&t.jsxs("div",{className:g==="error"?"devserver-preview-error-panel":"devserver-preview-blocked-panel","data-testid":"devserver-preview-fallback",role:"alert",children:[g==="error"?t.jsx(Ae,{className:"devserver-preview-blocked-icon","aria-hidden":"true"}):t.jsx(Ue,{className:"devserver-preview-blocked-icon","aria-hidden":"true"}),t.jsxs("div",{children:[t.jsx("p",{className:"devserver-preview-blocked-title",children:g==="error"?"Preview failed":"Preview blocked"}),re&&t.jsx("p",{className:"devserver-preview-blocked-context",children:re})]}),t.jsx("p",{className:"devserver-preview-blocked-description",children:"Open the preview in a new tab, or retry embedded mode after checking your server settings."}),t.jsxs("div",{className:"devserver-preview-blocked-actions",children:[t.jsx("button",{type:"button",className:"btn btn-primary",onClick:se,"data-testid":"devserver-preview-fallback-open-tab",children:"Open preview in new tab"}),t.jsx("button",{type:"button",className:"btn btn-sm",onClick:be,"data-testid":"devserver-preview-fallback-retry",children:"Retry embedded preview"})]})]}),h&&l==="embedded"&&!pe&&t.jsx(Ft,{url:h,embedStatus:g,onEmbedStatusChange:$e,iframeRef:W,blockReason:re,onRetry:be})]})]})]})}export{qt as DevServerView};
|