@kenkaiiii/ggcoder 4.11.2 → 4.12.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/app-sidecar.js +427 -62
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +5 -3
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +66 -11
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +219 -39
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/api-benchmark.d.ts +64 -0
- package/dist/core/api-benchmark.d.ts.map +1 -0
- package/dist/core/api-benchmark.js +381 -0
- package/dist/core/api-benchmark.js.map +1 -0
- package/dist/core/event-bus.d.ts +1 -0
- package/dist/core/event-bus.d.ts.map +1 -1
- package/dist/core/mcp/client.d.ts +32 -0
- package/dist/core/mcp/client.d.ts.map +1 -1
- package/dist/core/mcp/client.js +232 -27
- package/dist/core/mcp/client.js.map +1 -1
- package/dist/core/mcp/index.d.ts +3 -1
- package/dist/core/mcp/index.d.ts.map +1 -1
- package/dist/core/mcp/index.js +2 -0
- package/dist/core/mcp/index.js.map +1 -1
- package/dist/core/mcp/loopback.d.ts +27 -0
- package/dist/core/mcp/loopback.d.ts.map +1 -0
- package/dist/core/mcp/loopback.js +66 -0
- package/dist/core/mcp/loopback.js.map +1 -0
- package/dist/core/mcp/loopback.test.d.ts +2 -0
- package/dist/core/mcp/loopback.test.d.ts.map +1 -0
- package/dist/core/mcp/loopback.test.js +87 -0
- package/dist/core/mcp/loopback.test.js.map +1 -0
- package/dist/core/mcp/oauth-provider.d.ts +51 -0
- package/dist/core/mcp/oauth-provider.d.ts.map +1 -0
- package/dist/core/mcp/oauth-provider.js +95 -0
- package/dist/core/mcp/oauth-provider.js.map +1 -0
- package/dist/core/mcp/oauth-store.d.ts +39 -0
- package/dist/core/mcp/oauth-store.d.ts.map +1 -0
- package/dist/core/mcp/oauth-store.js +63 -0
- package/dist/core/mcp/oauth-store.js.map +1 -0
- package/dist/core/mcp/oauth-store.test.d.ts +2 -0
- package/dist/core/mcp/oauth-store.test.d.ts.map +1 -0
- package/dist/core/mcp/oauth-store.test.js +94 -0
- package/dist/core/mcp/oauth-store.test.js.map +1 -0
- package/dist/core/mcp/parse-add-command.d.ts.map +1 -1
- package/dist/core/mcp/parse-add-command.js +1 -0
- package/dist/core/mcp/parse-add-command.js.map +1 -1
- package/dist/core/mcp/parse-add-command.test.js +8 -2
- package/dist/core/mcp/parse-add-command.test.js.map +1 -1
- package/dist/core/mcp/store.d.ts +4 -4
- package/dist/core/mcp/store.d.ts.map +1 -1
- package/dist/core/mcp/store.js +7 -1
- package/dist/core/mcp/store.js.map +1 -1
- package/dist/core/mcp/store.test.js +11 -2
- package/dist/core/mcp/store.test.js.map +1 -1
- package/dist/core/mcp/types.d.ts +5 -1
- package/dist/core/mcp/types.d.ts.map +1 -1
- package/dist/core/process-manager.d.ts.map +1 -1
- package/dist/core/process-manager.js +5 -1
- package/dist/core/process-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +4 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +5 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/shell.d.ts +51 -0
- package/dist/core/shell.d.ts.map +1 -0
- package/dist/core/shell.js +82 -0
- package/dist/core/shell.js.map +1 -0
- package/dist/core/shell.test.d.ts +2 -0
- package/dist/core/shell.test.d.ts.map +1 -0
- package/dist/core/shell.test.js +87 -0
- package/dist/core/shell.test.js.map +1 -0
- package/dist/core/speed-benchmark.d.ts +133 -0
- package/dist/core/speed-benchmark.d.ts.map +1 -0
- package/dist/core/speed-benchmark.js +410 -0
- package/dist/core/speed-benchmark.js.map +1 -0
- package/dist/core/speed-benchmark.test.d.ts +2 -0
- package/dist/core/speed-benchmark.test.d.ts.map +1 -0
- package/dist/core/speed-benchmark.test.js +97 -0
- package/dist/core/speed-benchmark.test.js.map +1 -0
- package/dist/interactive.d.ts.map +1 -1
- package/dist/interactive.js +4 -3
- package/dist/interactive.js.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +17 -1
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit-diff.d.ts.map +1 -1
- package/dist/tools/edit-diff.js +25 -8
- package/dist/tools/edit-diff.js.map +1 -1
- package/dist/tools/generate-image.d.ts +39 -0
- package/dist/tools/generate-image.d.ts.map +1 -0
- package/dist/tools/generate-image.js +301 -0
- package/dist/tools/generate-image.js.map +1 -0
- package/dist/tools/generate-image.test.d.ts +2 -0
- package/dist/tools/generate-image.test.d.ts.map +1 -0
- package/dist/tools/generate-image.test.js +223 -0
- package/dist/tools/generate-image.test.js.map +1 -0
- package/dist/tools/index.d.ts +12 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +16 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/ls.d.ts.map +1 -1
- package/dist/tools/ls.js +7 -4
- package/dist/tools/ls.js.map +1 -1
- package/dist/tools/plan-mode.test.js +5 -5
- package/dist/tools/plan-mode.test.js.map +1 -1
- package/dist/tools/prompt-hints.d.ts.map +1 -1
- package/dist/tools/prompt-hints.js +2 -0
- package/dist/tools/prompt-hints.js.map +1 -1
- package/dist/tools/safe-env.d.ts.map +1 -1
- package/dist/tools/safe-env.js +27 -0
- package/dist/tools/safe-env.js.map +1 -1
- package/dist/ui/App.d.ts +1 -1
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/hooks/usePixelFixFlow.d.ts +1 -1
- package/dist/ui/hooks/usePixelFixFlow.d.ts.map +1 -1
- package/dist/ui/hooks/usePixelFixFlow.js +1 -1
- package/dist/ui/hooks/usePixelFixFlow.js.map +1 -1
- package/dist/ui/render.d.ts +1 -1
- package/dist/ui/render.d.ts.map +1 -1
- package/package.json +5 -5
package/dist/app-sidecar.js
CHANGED
|
@@ -38,8 +38,10 @@ import { loadTasksSync, saveTasksSync, getNextPendingTask, markTaskInProgress, }
|
|
|
38
38
|
import { initLogger, log } from "./core/logger.js";
|
|
39
39
|
import { RADIO_STATIONS, getCurrentStation, playRadio, stopRadio } from "./core/radio.js";
|
|
40
40
|
import { enrichProcessPath } from "./core/shell-path.js";
|
|
41
|
+
import { downscaleForPreview } from "./utils/image.js";
|
|
41
42
|
import { startServeMode } from "./modes/serve-mode.js";
|
|
42
43
|
import { loadTelegramConfig, saveTelegramConfig, verifyBotToken } from "./core/telegram-config.js";
|
|
44
|
+
import { loadServers, addServer, removeServer, getServer, parseMcpAddCommand, MCPClientManager, McpOAuthStore, } from "./core/mcp/index.js";
|
|
43
45
|
const ALL_PROVIDERS = [
|
|
44
46
|
"anthropic",
|
|
45
47
|
"xiaomi",
|
|
@@ -57,6 +59,11 @@ function appSettingsFile() {
|
|
|
57
59
|
function defaultProjectsRoot() {
|
|
58
60
|
return path.join(os.homedir(), "gg-projects");
|
|
59
61
|
}
|
|
62
|
+
/** Normalize a project cwd to a stable settings key so trailing slashes /
|
|
63
|
+
* relative segments collapse — the same project always maps to one entry. */
|
|
64
|
+
function projectModelKey(cwd) {
|
|
65
|
+
return path.resolve(cwd);
|
|
66
|
+
}
|
|
60
67
|
async function loadAppSettings() {
|
|
61
68
|
try {
|
|
62
69
|
const raw = JSON.parse(await fs.readFile(appSettingsFile(), "utf-8"));
|
|
@@ -64,6 +71,9 @@ async function loadAppSettings() {
|
|
|
64
71
|
projectsRoot: typeof raw.projectsRoot === "string" && raw.projectsRoot.trim()
|
|
65
72
|
? raw.projectsRoot
|
|
66
73
|
: defaultProjectsRoot(),
|
|
74
|
+
// Preserve the per-project map verbatim (validated + written by the
|
|
75
|
+
// model/thinking handlers below).
|
|
76
|
+
projectModels: raw.projectModels && typeof raw.projectModels === "object" ? raw.projectModels : undefined,
|
|
67
77
|
};
|
|
68
78
|
}
|
|
69
79
|
catch {
|
|
@@ -74,6 +84,19 @@ async function saveAppSettings(settings) {
|
|
|
74
84
|
await fs.mkdir(path.dirname(appSettingsFile()), { recursive: true });
|
|
75
85
|
await fs.writeFile(appSettingsFile(), JSON.stringify(settings, null, 2), "utf-8");
|
|
76
86
|
}
|
|
87
|
+
/** Read this project's persisted model/thinking prefs, if any. */
|
|
88
|
+
async function loadProjectModelPrefs(cwd) {
|
|
89
|
+
const s = await loadAppSettings();
|
|
90
|
+
return s.projectModels?.[projectModelKey(cwd)];
|
|
91
|
+
}
|
|
92
|
+
/** Persist this project's model/thinking prefs via read-modify-write so the rest
|
|
93
|
+
* of the settings file (projectsRoot, other projects' entries) is preserved. */
|
|
94
|
+
async function saveProjectModelPrefs(cwd, prefs) {
|
|
95
|
+
const s = await loadAppSettings();
|
|
96
|
+
const key = projectModelKey(cwd);
|
|
97
|
+
s.projectModels = { ...(s.projectModels ?? {}), [key]: prefs };
|
|
98
|
+
await saveAppSettings(s);
|
|
99
|
+
}
|
|
77
100
|
/**
|
|
78
101
|
* Persist the active model selection to ~/.gg/settings.json so it survives app
|
|
79
102
|
* restarts. Mirrors the CLI's handleModelSelect persistence (App.tsx).
|
|
@@ -247,6 +270,58 @@ function detectPromptCommand(text, candidates) {
|
|
|
247
270
|
}
|
|
248
271
|
return null;
|
|
249
272
|
}
|
|
273
|
+
/** A short transport summary for display (URL for http/sse, command+args for stdio). */
|
|
274
|
+
function mcpRowSummary(config) {
|
|
275
|
+
if (config.url)
|
|
276
|
+
return config.url;
|
|
277
|
+
return [config.command, ...(config.args ?? [])].filter(Boolean).join(" ");
|
|
278
|
+
}
|
|
279
|
+
/** Load + connect every server, returning one wire row per server. Mirrors the
|
|
280
|
+
* CLI dashboard's buildRows (connectAllDetailed, then dispose). Empty list
|
|
281
|
+
* short-circuits before spawning any stdio process / opening any HTTP conn. */
|
|
282
|
+
async function buildMcpRows(cwd) {
|
|
283
|
+
const scoped = await loadServers(cwd);
|
|
284
|
+
if (scoped.length === 0)
|
|
285
|
+
return [];
|
|
286
|
+
const manager = new MCPClientManager();
|
|
287
|
+
try {
|
|
288
|
+
const results = await manager.connectAllDetailed(scoped.map((s) => s.config));
|
|
289
|
+
return scoped.map((s) => {
|
|
290
|
+
const result = results.find((r) => r.name === s.config.name);
|
|
291
|
+
return {
|
|
292
|
+
name: s.config.name,
|
|
293
|
+
scope: s.scope,
|
|
294
|
+
ok: result?.ok ?? false,
|
|
295
|
+
toolCount: result?.toolCount ?? 0,
|
|
296
|
+
error: result?.error,
|
|
297
|
+
kind: s.config.url ? "http" : "stdio",
|
|
298
|
+
summary: mcpRowSummary(s.config),
|
|
299
|
+
requiresAuth: result?.requiresAuth,
|
|
300
|
+
};
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
await manager.dispose();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/** Probe a single server's connection before persisting it. Never throws — a
|
|
308
|
+
* failed probe returns ok:false with a human-readable error so the config can
|
|
309
|
+
* still be saved. Mirrors the CLI's probeServer. */
|
|
310
|
+
async function probeMcp(config) {
|
|
311
|
+
const manager = new MCPClientManager();
|
|
312
|
+
try {
|
|
313
|
+
const result = await manager.probe(config);
|
|
314
|
+
return {
|
|
315
|
+
ok: result.ok,
|
|
316
|
+
toolCount: result.toolCount,
|
|
317
|
+
error: result.error,
|
|
318
|
+
requiresAuth: result.requiresAuth,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
await manager.dispose();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
250
325
|
/**
|
|
251
326
|
* Sub-agents spawn the ggcoder CLI in JSON mode to run a delegated task. In the
|
|
252
327
|
* packaged desktop app the only runnable entry is THIS bundle (there's no
|
|
@@ -302,6 +377,25 @@ async function main() {
|
|
|
302
377
|
// ~/.gg/debug.log (initLogger truncates on each start).
|
|
303
378
|
const sidecarLog = path.join(paths.agentDir, "gg-app-sidecar.log");
|
|
304
379
|
initLogger(sidecarLog);
|
|
380
|
+
// Global last-resort guards, installed as early as the logger allows so they
|
|
381
|
+
// cover the WHOLE lifecycle — including startup/initialize, the phase the
|
|
382
|
+
// "sidecar did not start in time" bug lives in. The sidecar is a long-lived
|
|
383
|
+
// HTTP server the Rust shell can respawn: a stray rejection or thrown error
|
|
384
|
+
// from one request (e.g. an MCP probe spawning a misbehaving child) must not
|
|
385
|
+
// tear down the whole process and strand the window on its next call. Log and
|
|
386
|
+
// keep serving (mirrors astro/vscode/gstack long-lived-server handlers).
|
|
387
|
+
process.on("unhandledRejection", (reason) => {
|
|
388
|
+
log("ERROR", "app-sidecar", "unhandledRejection", {
|
|
389
|
+
message: reason instanceof Error ? reason.message : String(reason),
|
|
390
|
+
stack: reason instanceof Error ? reason.stack : undefined,
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
process.on("uncaughtException", (err) => {
|
|
394
|
+
log("ERROR", "app-sidecar", "uncaughtException", {
|
|
395
|
+
message: err.message,
|
|
396
|
+
stack: err.stack,
|
|
397
|
+
});
|
|
398
|
+
});
|
|
305
399
|
// The packaged desktop app launches from Finder/Dock with a minimal PATH that
|
|
306
400
|
// omits Homebrew/Cargo/version-manager dirs, so the agent can't find node,
|
|
307
401
|
// git, python, rg, etc. Enrich process.env.PATH from the login shell once,
|
|
@@ -311,19 +405,27 @@ async function main() {
|
|
|
311
405
|
const auth = new AuthStorage(paths.authFile);
|
|
312
406
|
await auth.load();
|
|
313
407
|
const saved = loadSavedSettings(paths.settingsFile);
|
|
314
|
-
|
|
408
|
+
// Per-project model/thinking prefs win over the shared global settings.json:
|
|
409
|
+
// each window (one project cwd) restores its own selection instead of every
|
|
410
|
+
// window reading the same single global slot that the last writer clobbered
|
|
411
|
+
// (the old bug — switching models in one window reset every other window).
|
|
412
|
+
const projectPrefs = await loadProjectModelPrefs(cwd);
|
|
413
|
+
const preferred = projectPrefs?.provider ?? saved.provider ?? "anthropic";
|
|
414
|
+
const savedModel = projectPrefs?.model ?? saved.model;
|
|
315
415
|
// Boot-tolerant: when no provider is configured this returns a logged-out
|
|
316
416
|
// fallback instead of throwing, so the sidecar still listens and the login
|
|
317
417
|
// endpoints are reachable for a fresh user (throwing here used to kill the
|
|
318
418
|
// sidecar before server.listen, making first-time login impossible).
|
|
319
|
-
const { provider, model, loggedIn } = await resolveStartOrFallback(auth, ALL_PROVIDERS, preferred,
|
|
419
|
+
const { provider, model, loggedIn } = await resolveStartOrFallback(auth, ALL_PROVIDERS, preferred, savedModel);
|
|
320
420
|
if (!loggedIn) {
|
|
321
421
|
log("WARN", "app-sidecar", "no provider configured — booting logged-out for login", {
|
|
322
422
|
fallbackProvider: provider,
|
|
323
423
|
});
|
|
324
424
|
}
|
|
325
|
-
|
|
326
|
-
|
|
425
|
+
// Per-project thinking prefs win over the global settings.json fallback.
|
|
426
|
+
const thinkEnabled = projectPrefs?.thinkingEnabled ?? saved.thinkingEnabled;
|
|
427
|
+
const thinkingLevel = thinkEnabled
|
|
428
|
+
? (projectPrefs?.thinkingLevel ?? saved.thinkingLevel ?? getMaxThinkingLevel(model))
|
|
327
429
|
: undefined;
|
|
328
430
|
// ── SSE fan-out (declared before the session so plan callbacks can use it) ─
|
|
329
431
|
const clients = new Set();
|
|
@@ -344,6 +446,12 @@ async function main() {
|
|
|
344
446
|
thinkingLevel,
|
|
345
447
|
sessionId: resumeSessionPath,
|
|
346
448
|
signal: abort.signal,
|
|
449
|
+
// The shell gates window readiness on the GG_APP_LISTENING handshake, which
|
|
450
|
+
// can't fire until initialize() resolves. Connect MCP in the background so a
|
|
451
|
+
// slow or hanging stdio server (e.g. a first-run `npx -y @playwright/mcp`
|
|
452
|
+
// download) can't delay the sidecar past the webview's startup timeout
|
|
453
|
+
// ("sidecar did not start in time"). Tools attach when the servers come up.
|
|
454
|
+
backgroundMcpConnect: true,
|
|
347
455
|
// Plan mode: the agent's enter_plan/exit_plan tools drive these. We flip
|
|
348
456
|
// session plan state (rebuilds the system prompt + enforces read-only
|
|
349
457
|
// tools) and surface the transition to the webview.
|
|
@@ -393,6 +501,11 @@ async function main() {
|
|
|
393
501
|
session.eventBus.on("tool_call_start", (d) => broadcast("tool_call_start", d));
|
|
394
502
|
session.eventBus.on("tool_call_update", (d) => broadcast("tool_call_update", d));
|
|
395
503
|
session.eventBus.on("tool_call_end", (d) => broadcast("tool_call_end", d));
|
|
504
|
+
// Native server tools (e.g. Anthropic web_search) do NOT end the turn — text
|
|
505
|
+
// streams before and after them in the SAME turn. The webview must reset its
|
|
506
|
+
// streaming bubble here, or the two text blocks concatenate with no separator
|
|
507
|
+
// ("…command.Let me pull…"). Mirrors the TUI's server_tool_call handling.
|
|
508
|
+
session.eventBus.on("server_tool_call", (d) => broadcast("server_tool_call", d));
|
|
396
509
|
session.eventBus.on("turn_end", (d) => broadcast("turn_end", d));
|
|
397
510
|
session.eventBus.on("agent_done", (d) => broadcast("agent_done", d));
|
|
398
511
|
session.eventBus.on("error", (d) => broadcast("error", { message: d.error instanceof Error ? d.error.message : String(d.error) }));
|
|
@@ -584,6 +697,7 @@ async function main() {
|
|
|
584
697
|
ready: true,
|
|
585
698
|
thinkingLevel: session.getThinkingLevel() ?? null,
|
|
586
699
|
supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
|
|
700
|
+
supportsVideo: getModel(st.model)?.supportsVideo ?? false,
|
|
587
701
|
...footerExtras(),
|
|
588
702
|
});
|
|
589
703
|
return;
|
|
@@ -606,6 +720,7 @@ async function main() {
|
|
|
606
720
|
running,
|
|
607
721
|
thinkingLevel: session.getThinkingLevel() ?? null,
|
|
608
722
|
supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
|
|
723
|
+
supportsVideo: getModel(st.model)?.supportsVideo ?? false,
|
|
609
724
|
...footerExtras(),
|
|
610
725
|
},
|
|
611
726
|
})}\n\n`);
|
|
@@ -630,7 +745,9 @@ async function main() {
|
|
|
630
745
|
catch {
|
|
631
746
|
configured = false;
|
|
632
747
|
}
|
|
633
|
-
|
|
748
|
+
// Only projectsRoot + configured flag are webview-facing; the
|
|
749
|
+
// per-project model map is internal persistence, never shipped out.
|
|
750
|
+
json(res, 200, { projectsRoot: s.projectsRoot, configured });
|
|
634
751
|
})();
|
|
635
752
|
return;
|
|
636
753
|
}
|
|
@@ -648,7 +765,11 @@ async function main() {
|
|
|
648
765
|
json(res, 400, { error: "projectsRoot is required" });
|
|
649
766
|
return;
|
|
650
767
|
}
|
|
651
|
-
|
|
768
|
+
// Read-modify-write so the per-project model map survives a projectsRoot
|
|
769
|
+
// change (a naive overwrite would drop every window's saved model).
|
|
770
|
+
const s = await loadAppSettings();
|
|
771
|
+
s.projectsRoot = projectsRoot;
|
|
772
|
+
await saveAppSettings(s);
|
|
652
773
|
json(res, 200, { projectsRoot });
|
|
653
774
|
});
|
|
654
775
|
return;
|
|
@@ -727,60 +848,126 @@ async function main() {
|
|
|
727
848
|
return;
|
|
728
849
|
}
|
|
729
850
|
if (method === "GET" && url === "/history") {
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
//
|
|
733
|
-
//
|
|
734
|
-
//
|
|
735
|
-
// prompt body — matching how they appear live.
|
|
851
|
+
// Reconstruct the transcript from persisted messages so resume is 1:1 with
|
|
852
|
+
// the live SSE stream. Walks ALL message types (not just user/assistant):
|
|
853
|
+
// tool result messages carry ImageContent blocks (screenshots,
|
|
854
|
+
// generate_image) that must re-render inline, and assistant tool_call
|
|
855
|
+
// blocks carry sub-agent delegations that must re-appear as group items.
|
|
736
856
|
//
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
//
|
|
857
|
+
// The `details` object (imagePreviews with path + downscaled preview) is
|
|
858
|
+
// event-only and never persisted — we reconstruct from the raw
|
|
859
|
+
// ImageContent in the tool result, downsampling on the sidecar side and
|
|
860
|
+
// extracting the path from the text block ("Generated image → /path").
|
|
740
861
|
void (async () => {
|
|
741
862
|
const commandCandidates = [...PROMPT_COMMANDS, ...(await loadCustomCommands(cwd))];
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
863
|
+
const messages = session.getMessages();
|
|
864
|
+
// Pre-index tool results by toolCallId so we can pair tool calls with
|
|
865
|
+
// their results (for sub-agent status + image extraction).
|
|
866
|
+
const toolResultMap = new Map();
|
|
867
|
+
for (const msg of messages) {
|
|
868
|
+
if (msg.role !== "tool")
|
|
869
|
+
continue;
|
|
870
|
+
for (const tr of msg.content) {
|
|
871
|
+
toolResultMap.set(tr.toolCallId, {
|
|
872
|
+
content: tr.content,
|
|
873
|
+
isError: tr.isError ?? false,
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
const history = [];
|
|
878
|
+
for (const msg of messages) {
|
|
879
|
+
if (msg.role === "system")
|
|
880
|
+
continue;
|
|
881
|
+
if (msg.role === "tool") {
|
|
882
|
+
// Tool result messages: check for ImageContent blocks (screenshots,
|
|
883
|
+
// generated images) and emit a toolImages entry.
|
|
884
|
+
for (const tr of msg.content) {
|
|
885
|
+
if (typeof tr.content === "string")
|
|
886
|
+
continue;
|
|
887
|
+
const imageBlocks = tr.content.filter((c) => c.type === "image");
|
|
888
|
+
if (imageBlocks.length === 0)
|
|
889
|
+
continue;
|
|
890
|
+
// Extract the path from the text block (e.g. "Generated image → /path").
|
|
891
|
+
const textBlock = tr.content.find((c) => c.type === "text" && "text" in c && typeof c.text === "string");
|
|
892
|
+
const textContent = textBlock && textBlock.type === "text" ? textBlock.text : "";
|
|
893
|
+
const pathMatch = textContent.match(/→\s*(\S+)/);
|
|
894
|
+
const imgPath = pathMatch?.[1];
|
|
895
|
+
// Downscale each image for the webview preview.
|
|
896
|
+
const toolImages = [];
|
|
897
|
+
for (const block of imageBlocks) {
|
|
898
|
+
if (block.type !== "image")
|
|
899
|
+
continue;
|
|
900
|
+
try {
|
|
901
|
+
const rawBuf = Buffer.from(block.data, "base64");
|
|
902
|
+
const previewBuf = await downscaleForPreview(rawBuf);
|
|
903
|
+
toolImages.push({
|
|
904
|
+
src: `data:${block.mediaType};base64,${previewBuf.toString("base64")}`,
|
|
905
|
+
path: imgPath,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
catch {
|
|
909
|
+
// Downscale failed — use the raw data.
|
|
910
|
+
toolImages.push({
|
|
911
|
+
src: `data:${block.mediaType};base64,${block.data}`,
|
|
912
|
+
path: imgPath,
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (toolImages.length > 0) {
|
|
917
|
+
history.push({
|
|
918
|
+
role: "assistant",
|
|
919
|
+
text: "",
|
|
920
|
+
toolImages,
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
// User or assistant message — existing text/hook/command/compacted
|
|
927
|
+
// extraction, plus sub-agent group detection for assistant tool_calls.
|
|
928
|
+
const text = typeof msg.content === "string"
|
|
929
|
+
? msg.content
|
|
930
|
+
: msg.content
|
|
753
931
|
.map((c) => c.type === "text" && "text" in c && typeof c.text === "string" ? c.text : "")
|
|
754
932
|
.join("");
|
|
755
|
-
|
|
756
|
-
// resume — the webview only ever saw the live SSE stream, and the
|
|
757
|
-
// persisted message holds the base64 bytes. Without this, attached
|
|
758
|
-
// images vanish when returning to a session.
|
|
759
|
-
const images = typeof m.content === "string"
|
|
933
|
+
const images = typeof msg.content === "string"
|
|
760
934
|
? []
|
|
761
|
-
:
|
|
762
|
-
const hook =
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
// context" notice instead of dumping the full summary body.
|
|
766
|
-
const compacted = m.role === "user" && !hook && text.startsWith("[Previous conversation summary]");
|
|
767
|
-
// Recover a `/name [args]` command invocation from its expanded body
|
|
768
|
-
// (skip messages already claimed as hooks or compaction summaries).
|
|
769
|
-
const command = m.role === "user" && !hook && !compacted
|
|
935
|
+
: msg.content.flatMap((c) => c.type === "image" ? [`data:${c.mediaType};base64,${c.data}`] : []);
|
|
936
|
+
const hook = msg.role === "user" ? detectHookKind(text) : null;
|
|
937
|
+
const compacted = msg.role === "user" && !hook && text.startsWith("[Previous conversation summary]");
|
|
938
|
+
const command = msg.role === "user" && !hook && !compacted
|
|
770
939
|
? detectPromptCommand(text, commandCandidates)
|
|
771
940
|
: null;
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
//
|
|
783
|
-
|
|
941
|
+
if (text.trim() || images.length > 0) {
|
|
942
|
+
history.push({
|
|
943
|
+
role: msg.role,
|
|
944
|
+
text: command ?? text,
|
|
945
|
+
images,
|
|
946
|
+
hook,
|
|
947
|
+
command: command !== null,
|
|
948
|
+
compacted,
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
// Assistant tool_call blocks: detect sub-agent delegations.
|
|
952
|
+
if (msg.role === "assistant" && typeof msg.content !== "string") {
|
|
953
|
+
const subagentCalls = msg.content.filter((c) => c.type === "tool_call" && c.name === "subagent");
|
|
954
|
+
if (subagentCalls.length > 0) {
|
|
955
|
+
const agents = subagentCalls.map((c) => {
|
|
956
|
+
const result = toolResultMap.get(c.id);
|
|
957
|
+
return {
|
|
958
|
+
agentName: typeof c.args?.agent === "string" ? c.args.agent : undefined,
|
|
959
|
+
status: result?.isError ? "error" : "done",
|
|
960
|
+
toolUseCount: 0,
|
|
961
|
+
};
|
|
962
|
+
});
|
|
963
|
+
history.push({
|
|
964
|
+
role: "assistant",
|
|
965
|
+
text: "",
|
|
966
|
+
subagentGroup: agents,
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
784
971
|
json(res, 200, { history });
|
|
785
972
|
})();
|
|
786
973
|
return;
|
|
@@ -827,13 +1014,11 @@ async function main() {
|
|
|
827
1014
|
return;
|
|
828
1015
|
}
|
|
829
1016
|
if (running) {
|
|
830
|
-
// Queue
|
|
831
|
-
//
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
}
|
|
836
|
-
const count = session.queueMessage(text);
|
|
1017
|
+
// Queue prompts as mid-run steering (mirrors the CLI). Attachments are
|
|
1018
|
+
// persisted to .gg/uploads first so the queued media rides the same
|
|
1019
|
+
// native-block path as a non-queued attachment prompt when it drains.
|
|
1020
|
+
const prepared = attachments.length > 0 ? await prepareAttachments(cwd, attachments) : [];
|
|
1021
|
+
const count = session.queueMessage(text, prepared);
|
|
837
1022
|
broadcast("queued", { count });
|
|
838
1023
|
json(res, 202, { queued: true, count });
|
|
839
1024
|
return;
|
|
@@ -979,7 +1164,16 @@ async function main() {
|
|
|
979
1164
|
if (prevLevel && !isThinkingLevelSupported(target.provider, target.id, prevLevel)) {
|
|
980
1165
|
session.setThinkingLevel(getNextThinkingLevel(target.provider, target.id, undefined));
|
|
981
1166
|
}
|
|
982
|
-
// Persist so
|
|
1167
|
+
// Persist per-project so THIS window/project restores its own model on
|
|
1168
|
+
// restart (not the single global slot every window shares). Keep the
|
|
1169
|
+
// global write too as a "last used" fallback for never-opened projects
|
|
1170
|
+
// and so the CLI stays in sync.
|
|
1171
|
+
await saveProjectModelPrefs(cwd, {
|
|
1172
|
+
provider: target.provider,
|
|
1173
|
+
model: target.id,
|
|
1174
|
+
thinkingEnabled: !!session.getThinkingLevel(),
|
|
1175
|
+
thinkingLevel: session.getThinkingLevel() ?? undefined,
|
|
1176
|
+
});
|
|
983
1177
|
await persistModelSelection(paths.settingsFile, target.provider, target.id);
|
|
984
1178
|
await persistThinkingLevel(paths.settingsFile, session.getThinkingLevel());
|
|
985
1179
|
const payload = {
|
|
@@ -1021,8 +1215,14 @@ async function main() {
|
|
|
1021
1215
|
const st = session.getState();
|
|
1022
1216
|
const next = getNextThinkingLevel(st.provider, st.model, session.getThinkingLevel());
|
|
1023
1217
|
session.setThinkingLevel(next);
|
|
1024
|
-
// Persist so
|
|
1025
|
-
|
|
1218
|
+
// Persist per-project so THIS window restores its thinking state on
|
|
1219
|
+
// restart; keep the global write as a fallback (mirrors the CLI).
|
|
1220
|
+
void saveProjectModelPrefs(cwd, {
|
|
1221
|
+
provider: st.provider,
|
|
1222
|
+
model: st.model,
|
|
1223
|
+
thinkingEnabled: !!next,
|
|
1224
|
+
thinkingLevel: next ?? undefined,
|
|
1225
|
+
}).then(() => persistThinkingLevel(paths.settingsFile, next));
|
|
1026
1226
|
const payload = {
|
|
1027
1227
|
thinkingLevel: next ?? null,
|
|
1028
1228
|
supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
|
|
@@ -1321,6 +1521,171 @@ async function main() {
|
|
|
1321
1521
|
})();
|
|
1322
1522
|
return;
|
|
1323
1523
|
}
|
|
1524
|
+
// ── MCP server management (mirrors `ggcoder mcp`) ──────────────────
|
|
1525
|
+
// `targetCwd` (project scope) overrides the window cwd so a server can be
|
|
1526
|
+
// added/removed for ANY discovered project, not just this window's. Global
|
|
1527
|
+
// scope ignores it (always ~/.gg/mcp.json).
|
|
1528
|
+
if (method === "GET" && (url === "/mcp" || url.startsWith("/mcp?"))) {
|
|
1529
|
+
const targetCwd = new URL(url, `http://${host}`).searchParams.get("cwd") ?? cwd;
|
|
1530
|
+
void buildMcpRows(targetCwd)
|
|
1531
|
+
.then((servers) => json(res, 200, { servers }))
|
|
1532
|
+
.catch((err) => {
|
|
1533
|
+
log("ERROR", "app-sidecar", "buildMcpRows failed", {
|
|
1534
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1535
|
+
});
|
|
1536
|
+
json(res, 200, { servers: [] });
|
|
1537
|
+
});
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
if (method === "POST" && url === "/mcp/add") {
|
|
1541
|
+
void readBody(req).then(async (raw) => {
|
|
1542
|
+
let line;
|
|
1543
|
+
let scopeValue;
|
|
1544
|
+
let bodyCwd;
|
|
1545
|
+
try {
|
|
1546
|
+
const body = JSON.parse(raw);
|
|
1547
|
+
line = body.line ?? "";
|
|
1548
|
+
scopeValue = body.scope ?? "global";
|
|
1549
|
+
bodyCwd = body.cwd;
|
|
1550
|
+
}
|
|
1551
|
+
catch {
|
|
1552
|
+
json(res, 400, { error: "invalid JSON body" });
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
const scope = scopeValue === "project" ? "project" : "global";
|
|
1556
|
+
if (scope === "project" && !bodyCwd) {
|
|
1557
|
+
json(res, 400, { error: "project scope requires a project (cwd)." });
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
const targetCwd = bodyCwd ?? cwd;
|
|
1561
|
+
const parsed = parseMcpAddCommand(line);
|
|
1562
|
+
if (!parsed.ok) {
|
|
1563
|
+
json(res, 400, { error: parsed.error });
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
const config = parsed.value.config;
|
|
1567
|
+
try {
|
|
1568
|
+
// Best-effort probe — never blocks the save. A failed connect is
|
|
1569
|
+
// surfaced to the UI but the config is still persisted (mirrors the
|
|
1570
|
+
// CLI). probeMcp swallows connect errors; the try/catch guards the
|
|
1571
|
+
// persist step so a write failure returns a 500 instead of becoming
|
|
1572
|
+
// an unhandled rejection that would crash the sidecar.
|
|
1573
|
+
const probe = await probeMcp(config);
|
|
1574
|
+
const saved = await addServer(config, scope, targetCwd, true);
|
|
1575
|
+
if (!saved.ok) {
|
|
1576
|
+
json(res, 400, { error: saved.error });
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
json(res, 200, {
|
|
1580
|
+
ok: true,
|
|
1581
|
+
name: config.name,
|
|
1582
|
+
connected: probe.ok,
|
|
1583
|
+
toolCount: probe.toolCount,
|
|
1584
|
+
error: probe.error,
|
|
1585
|
+
requiresAuth: probe.requiresAuth,
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
catch (err) {
|
|
1589
|
+
json(res, 500, {
|
|
1590
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
if (method === "POST" && url === "/mcp/remove") {
|
|
1597
|
+
void readBody(req).then(async (raw) => {
|
|
1598
|
+
let name;
|
|
1599
|
+
let scopeValue;
|
|
1600
|
+
let bodyCwd;
|
|
1601
|
+
try {
|
|
1602
|
+
const body = JSON.parse(raw);
|
|
1603
|
+
name = body.name ?? "";
|
|
1604
|
+
scopeValue = body.scope ?? "global";
|
|
1605
|
+
bodyCwd = body.cwd;
|
|
1606
|
+
}
|
|
1607
|
+
catch {
|
|
1608
|
+
json(res, 400, { error: "invalid JSON body" });
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
if (!name.trim()) {
|
|
1612
|
+
json(res, 400, { error: "missing server name" });
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
const scope = scopeValue === "project" ? "project" : "global";
|
|
1616
|
+
if (scope === "project" && !bodyCwd) {
|
|
1617
|
+
json(res, 400, { error: "project scope requires a project (cwd)." });
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
const targetCwd = bodyCwd ?? cwd;
|
|
1621
|
+
const removed = await removeServer(name, scope, targetCwd);
|
|
1622
|
+
// Drop any saved OAuth tokens for this server so a re-add starts clean.
|
|
1623
|
+
await new McpOAuthStore().clear(name).catch(() => { });
|
|
1624
|
+
json(res, 200, { removed });
|
|
1625
|
+
});
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
// Interactive OAuth login for a remote (HTTP) MCP server. The browser is
|
|
1629
|
+
// opened by the webview in response to the broadcast `mcp_auth_url` event;
|
|
1630
|
+
// progress + outcome stream via `mcp_auth_status` / `mcp_auth_done` /
|
|
1631
|
+
// `mcp_auth_error`. Responds 202 immediately and runs the flow in the
|
|
1632
|
+
// background (the browser round-trip can take a while).
|
|
1633
|
+
if (method === "POST" && url === "/mcp/login") {
|
|
1634
|
+
void readBody(req).then(async (raw) => {
|
|
1635
|
+
let name;
|
|
1636
|
+
let scopeValue;
|
|
1637
|
+
let bodyCwd;
|
|
1638
|
+
try {
|
|
1639
|
+
const body = JSON.parse(raw);
|
|
1640
|
+
name = body.name ?? "";
|
|
1641
|
+
scopeValue = body.scope ?? "global";
|
|
1642
|
+
bodyCwd = body.cwd;
|
|
1643
|
+
}
|
|
1644
|
+
catch {
|
|
1645
|
+
json(res, 400, { error: "invalid JSON body" });
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
if (!name.trim()) {
|
|
1649
|
+
json(res, 400, { error: "missing server name" });
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
const scope = scopeValue === "project" ? "project" : "global";
|
|
1653
|
+
const targetCwd = bodyCwd ?? cwd;
|
|
1654
|
+
const scoped = await getServer(name, targetCwd);
|
|
1655
|
+
if (!scoped || scoped.scope !== scope) {
|
|
1656
|
+
json(res, 404, { error: `No "${name}" server found.` });
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
if (!scoped.config.url) {
|
|
1660
|
+
json(res, 400, { error: "Login is only supported for HTTP MCP servers." });
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
json(res, 202, { accepted: true });
|
|
1664
|
+
broadcast("mcp_auth_status", { name, message: "Starting login\u2026" });
|
|
1665
|
+
const manager = new MCPClientManager();
|
|
1666
|
+
try {
|
|
1667
|
+
const result = await manager.login(scoped.config, (authUrl) => {
|
|
1668
|
+
broadcast("mcp_auth_url", { name, url: authUrl });
|
|
1669
|
+
});
|
|
1670
|
+
if (result.ok) {
|
|
1671
|
+
broadcast("mcp_auth_done", { name, toolCount: result.toolCount });
|
|
1672
|
+
}
|
|
1673
|
+
else {
|
|
1674
|
+
broadcast("mcp_auth_error", { name, message: result.error ?? "Login failed." });
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
catch (err) {
|
|
1678
|
+
broadcast("mcp_auth_error", {
|
|
1679
|
+
name,
|
|
1680
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
finally {
|
|
1684
|
+
await manager.dispose().catch(() => { });
|
|
1685
|
+
}
|
|
1686
|
+
});
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1324
1689
|
json(res, 404, { error: "not found" });
|
|
1325
1690
|
});
|
|
1326
1691
|
server.listen(port, host, () => {
|