@kenkaiiii/ggcoder 5.23.3 → 5.25.0
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 +318 -12
- package/dist/app-sidecar.js.map +1 -1
- package/dist/core/agent-session-add-dir.test.d.ts +2 -0
- package/dist/core/agent-session-add-dir.test.d.ts.map +1 -0
- package/dist/core/agent-session-add-dir.test.js +110 -0
- package/dist/core/agent-session-add-dir.test.js.map +1 -0
- package/dist/core/agent-session.d.ts +29 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +74 -4
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/local-endpoint-store.d.ts +50 -0
- package/dist/core/local-endpoint-store.d.ts.map +1 -0
- package/dist/core/local-endpoint-store.js +201 -0
- package/dist/core/local-endpoint-store.js.map +1 -0
- package/dist/core/local-endpoint-store.test.d.ts +2 -0
- package/dist/core/local-endpoint-store.test.d.ts.map +1 -0
- package/dist/core/local-endpoint-store.test.js +131 -0
- package/dist/core/local-endpoint-store.test.js.map +1 -0
- package/dist/core/network-guard.d.ts +47 -0
- package/dist/core/network-guard.d.ts.map +1 -0
- package/dist/core/network-guard.js +208 -0
- package/dist/core/network-guard.js.map +1 -0
- package/dist/core/network-guard.test.d.ts +2 -0
- package/dist/core/network-guard.test.d.ts.map +1 -0
- package/dist/core/network-guard.test.js +72 -0
- package/dist/core/network-guard.test.js.map +1 -0
- package/dist/core/prompt-commands.d.ts.map +1 -1
- package/dist/core/prompt-commands.js +55 -36
- package/dist/core/prompt-commands.js.map +1 -1
- package/dist/core/prompt-commands.test.js +49 -2
- package/dist/core/prompt-commands.test.js.map +1 -1
- package/dist/core/session-export.d.ts +98 -0
- package/dist/core/session-export.d.ts.map +1 -0
- package/dist/core/session-export.js +356 -0
- package/dist/core/session-export.js.map +1 -0
- package/dist/core/session-export.test.d.ts +2 -0
- package/dist/core/session-export.test.d.ts.map +1 -0
- package/dist/core/session-export.test.js +353 -0
- package/dist/core/session-export.test.js.map +1 -0
- package/dist/core/settings-manager.d.ts +5 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +10 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts +18 -0
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +32 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/slash-commands.test.d.ts +2 -0
- package/dist/core/slash-commands.test.d.ts.map +1 -0
- package/dist/core/slash-commands.test.js +59 -0
- package/dist/core/slash-commands.test.js.map +1 -0
- package/dist/core/workspace-guard.d.ts +2 -0
- package/dist/core/workspace-guard.d.ts.map +1 -1
- package/dist/core/workspace-guard.js +4 -1
- package/dist/core/workspace-guard.js.map +1 -1
- package/dist/core/workspace-guard.test.js +21 -0
- package/dist/core/workspace-guard.test.js.map +1 -1
- package/dist/system-prompt.d.ts +11 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +19 -4
- package/dist/system-prompt.js.map +1 -1
- package/dist/system-prompt.test.js +9 -0
- package/dist/system-prompt.test.js.map +1 -1
- package/dist/tools/bash.d.ts +2 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +9 -1
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/bash.test.js +19 -0
- package/dist/tools/bash.test.js.map +1 -1
- package/dist/tools/grep.d.ts +3 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +132 -10
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/grep.test.js +53 -0
- package/dist/tools/grep.test.js.map +1 -1
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/web-fetch.d.ts +2 -1
- package/dist/tools/web-fetch.d.ts.map +1 -1
- package/dist/tools/web-fetch.js +17 -6
- package/dist/tools/web-fetch.js.map +1 -1
- package/dist/tools/web-fetch.test.js +34 -0
- package/dist/tools/web-fetch.test.js.map +1 -1
- package/dist/tools/web-search.d.ts +2 -1
- package/dist/tools/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js +21 -5
- package/dist/tools/web-search.js.map +1 -1
- package/package.json +4 -4
package/dist/app-sidecar.js
CHANGED
|
@@ -34,10 +34,12 @@ import { isWorkflowCommandText, countAssistantMessages, shouldStartAutopilotCycl
|
|
|
34
34
|
import { driveAutopilotCycle, frameAutopilotInjection } from "./core/autopilot-cycle.js";
|
|
35
35
|
import { validateKenModelPref, effectiveKenModel } from "./core/ken-model.js";
|
|
36
36
|
import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeKenTurnsForHistory, restoreUserRow, restoreAssistantTexts, autopilotMarkerCopySeed, } from "./core/session-history.js";
|
|
37
|
+
import { sessionToMarkdown, defaultExportFilename, } from "./core/session-export.js";
|
|
37
38
|
import { AuthStorage } from "./core/auth-storage.js";
|
|
38
39
|
import { cleanupToolOutputs } from "./tools/overflow.js";
|
|
39
40
|
import { readCappedBody } from "./utils/http-body.js";
|
|
40
|
-
import { fetchSubscriptionUsage, MOONSHOT_OAUTH_KEY, SubscriptionUsageError, XIAOMI_CREDITS_KEY, } from "@kenkaiiii/gg-core";
|
|
41
|
+
import { fetchSubscriptionUsage, MOONSHOT_OAUTH_KEY, SubscriptionUsageError, XIAOMI_CREDITS_KEY, discoverLocalModels, findProbedModel, formatLocalModelId, parseLocalModelId, probeEndpoint, toModelInfo as localModelInfo, } from "@kenkaiiii/gg-core";
|
|
42
|
+
import { LocalEndpointError, addCustomEndpoint, listAllEndpoints, removeCustomEndpoint, syncEndpointCredentials, } from "./core/local-endpoint-store.js";
|
|
41
43
|
import { loginAnthropic } from "./core/oauth/anthropic.js";
|
|
42
44
|
import { loginOpenAI } from "./core/oauth/openai.js";
|
|
43
45
|
import { loginGemini } from "./core/oauth/gemini.js";
|
|
@@ -45,7 +47,7 @@ import { loginKimi } from "./core/oauth/kimi.js";
|
|
|
45
47
|
import { AUTH_PROVIDERS } from "./core/auth-providers.js";
|
|
46
48
|
import { ensureAppDirs, loadSavedSettings } from "./config.js";
|
|
47
49
|
import { SettingsManager } from "./core/settings-manager.js";
|
|
48
|
-
import { getModel, getDefaultThinkingLevel, getContextWindow,
|
|
50
|
+
import { getModel, getDefaultThinkingLevel, getContextWindow, getAllModels, clearRuntimeModels, registerRuntimeModels, } from "./core/model-registry.js";
|
|
49
51
|
import { resolveStartOrFallback } from "./core/resolve-start.js";
|
|
50
52
|
import { getGitBranch, getGitDirtyFileCount, isGitRepo } from "./utils/git.js";
|
|
51
53
|
import { getGitHubOpenCounts, getGitHubRepoSlug } from "./utils/github.js";
|
|
@@ -346,6 +348,11 @@ async function searchProjectFiles(cwd, rawQuery) {
|
|
|
346
348
|
* hook prompt, by its distinctive opening phrase. Returns the hook kind so the
|
|
347
349
|
* webview can render the short notice line instead of the full prompt body.
|
|
348
350
|
*/
|
|
351
|
+
/** `?tools=` on /export. Anything unrecognised (including absent) falls back to
|
|
352
|
+
* the human-facing `summary` default rather than dumping every payload. */
|
|
353
|
+
function parseToolDetail(value) {
|
|
354
|
+
return value === "none" || value === "full" ? value : "summary";
|
|
355
|
+
}
|
|
349
356
|
function detectHookKind(text) {
|
|
350
357
|
const t = text.trimStart();
|
|
351
358
|
if (t.startsWith("Ideal? Review the actual work"))
|
|
@@ -614,6 +621,13 @@ async function main() {
|
|
|
614
621
|
});
|
|
615
622
|
const usageCache = new Map();
|
|
616
623
|
const usageRequests = new Map();
|
|
624
|
+
// Last snapshot that actually carried windows, per provider. Replayed while a
|
|
625
|
+
// fetch is failing so the title meter never blinks out of existence. Bounded
|
|
626
|
+
// by USAGE_LAST_GOOD_MAX_AGE_MS — a provider that never recovers must stop
|
|
627
|
+
// reporting rather than freeze a percentage (and a long-past reset time) on
|
|
628
|
+
// screen forever. The 429 backoff alone runs to 24h, far past any usefulness.
|
|
629
|
+
const usageLastGood = new Map();
|
|
630
|
+
const USAGE_LAST_GOOD_MAX_AGE_MS = 30 * 60_000;
|
|
617
631
|
// 429 backoff: quota endpoints are auxiliary UI data. Honor Retry-After when
|
|
618
632
|
// provided; otherwise retain the unavailable snapshot for 30 minutes. Clamp
|
|
619
633
|
// the provider value so a malformed header can neither hammer the endpoint
|
|
@@ -628,6 +642,9 @@ async function main() {
|
|
|
628
642
|
// Moonshot platform API key is metered per-token, not per plan window.
|
|
629
643
|
const authKey = provider === "moonshot" ? MOONSHOT_OAUTH_KEY : provider;
|
|
630
644
|
if (!(await auth.hasProviderAuth(authKey))) {
|
|
645
|
+
// Logged out: drop the replay cache so a later login on a DIFFERENT
|
|
646
|
+
// account can never inherit the previous one's numbers.
|
|
647
|
+
usageLastGood.delete(provider);
|
|
631
648
|
return { provider, displayName, connected: false, windows: [], fetchedAt: Date.now() };
|
|
632
649
|
}
|
|
633
650
|
try {
|
|
@@ -671,6 +688,18 @@ async function main() {
|
|
|
671
688
|
...(backoffMs !== undefined && { backoffMs: String(backoffMs) }),
|
|
672
689
|
});
|
|
673
690
|
const connected = await auth.hasProviderAuth(authKey);
|
|
691
|
+
// Transient failures (notably the 429s these auxiliary quota endpoints
|
|
692
|
+
// hand out) must not blank the meter. Keep serving the last good
|
|
693
|
+
// snapshot, flagged `stale`, so the bar stays put instead of flickering
|
|
694
|
+
// out for the whole backoff window and back in on the next success.
|
|
695
|
+
// Past the max age it's dropped — no data beats confidently wrong data.
|
|
696
|
+
const lastGood = usageLastGood.get(provider);
|
|
697
|
+
if (lastGood && Date.now() - lastGood.fetchedAt >= USAGE_LAST_GOOD_MAX_AGE_MS) {
|
|
698
|
+
usageLastGood.delete(provider);
|
|
699
|
+
}
|
|
700
|
+
else if (connected && lastGood) {
|
|
701
|
+
return { ...lastGood, stale: true };
|
|
702
|
+
}
|
|
674
703
|
return {
|
|
675
704
|
provider,
|
|
676
705
|
displayName,
|
|
@@ -692,6 +721,11 @@ async function main() {
|
|
|
692
721
|
usageRequests.set(provider, request);
|
|
693
722
|
try {
|
|
694
723
|
const result = await request;
|
|
724
|
+
// Never re-store a replay — it would keep its original `fetchedAt`, but
|
|
725
|
+
// writing it back muddies the "last GOOD" contract for no gain.
|
|
726
|
+
if (result.connected && !result.error && !result.stale && result.windows.length > 0) {
|
|
727
|
+
usageLastGood.set(provider, result);
|
|
728
|
+
}
|
|
695
729
|
// Anthropic can return utilization before it assigns reset timestamps
|
|
696
730
|
// (notably before the account's first active request). Retry that partial
|
|
697
731
|
// snapshot quickly; complete snapshots keep the normal one-minute cache.
|
|
@@ -1110,6 +1144,25 @@ async function createSession(deps, opts) {
|
|
|
1110
1144
|
// /help
|
|
1111
1145
|
.replaceAll(/see \/help/gi, "check the help menu"));
|
|
1112
1146
|
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Guidance for a connection failure against the ACTIVE local endpoint, or
|
|
1149
|
+
* undefined when that isn't the situation (so the generic wording stands).
|
|
1150
|
+
* "Disable your VPN / allow us through the firewall" is useless advice for a
|
|
1151
|
+
* server running on this machine — name what the user has to restart.
|
|
1152
|
+
*/
|
|
1153
|
+
function localNetworkGuidance(source) {
|
|
1154
|
+
if (source !== "network")
|
|
1155
|
+
return undefined;
|
|
1156
|
+
const state = session.getState();
|
|
1157
|
+
if (state.provider !== "local")
|
|
1158
|
+
return undefined;
|
|
1159
|
+
const parsed = parseLocalModelId(state.model);
|
|
1160
|
+
const probe = localProbes.find((p) => p.endpoint.id === parsed?.endpointId);
|
|
1161
|
+
if (!probe)
|
|
1162
|
+
return undefined;
|
|
1163
|
+
return (`${probe.endpoint.label} stopped responding at ${probe.endpoint.baseUrl}. ` +
|
|
1164
|
+
"Start it again and retry, or pick another model.");
|
|
1165
|
+
}
|
|
1113
1166
|
// Turn any thrown value into the same clear headline/message/guidance shape
|
|
1114
1167
|
// the TUI shows (see gg-ai's formatError) instead of a bare `err.message`, log
|
|
1115
1168
|
// the full detail, and broadcast it under `type` ("error" or "ken_error").
|
|
@@ -1119,7 +1172,7 @@ async function createSession(deps, opts) {
|
|
|
1119
1172
|
function broadcastError(type, logLabel, err) {
|
|
1120
1173
|
const f = formatError(err);
|
|
1121
1174
|
const message = f.message ? desktopGuidance(f.message) : undefined;
|
|
1122
|
-
const guidance = desktopGuidance(f.guidance);
|
|
1175
|
+
const guidance = localNetworkGuidance(f.source) ?? desktopGuidance(f.guidance);
|
|
1123
1176
|
captureSidecarError(err, `app-sidecar.${logLabel.replaceAll(" ", "-")}`, {
|
|
1124
1177
|
scope: type,
|
|
1125
1178
|
...(f.provider ? { provider: f.provider } : {}),
|
|
@@ -1227,6 +1280,99 @@ async function createSession(deps, opts) {
|
|
|
1227
1280
|
}
|
|
1228
1281
|
}
|
|
1229
1282
|
log("INFO", "app-sidecar", "session ready", { provider, model, mode, chatAgent, cwd });
|
|
1283
|
+
// ── Local models (Ollama / LM Studio / llama.cpp / vLLM) ──
|
|
1284
|
+
// Probing four HTTP endpoints must never delay readiness, so this runs in the
|
|
1285
|
+
// background (same shape as backgroundMcpConnect) and pushes a models_change
|
|
1286
|
+
// frame when it lands.
|
|
1287
|
+
let localProbes = [];
|
|
1288
|
+
async function scanLocalModels(force) {
|
|
1289
|
+
const endpoints = await listAllEndpoints();
|
|
1290
|
+
const { probes, models } = await discoverLocalModels(endpoints, { force });
|
|
1291
|
+
localProbes = probes;
|
|
1292
|
+
// Only endpoints that answered get a credential: writing one for a server
|
|
1293
|
+
// that isn't running would make `hasProviderAuth("local")` true forever.
|
|
1294
|
+
await syncEndpointCredentials(probes.filter((probe) => probe.reachable).map((probe) => probe.endpoint));
|
|
1295
|
+
clearRuntimeModels((m) => m.provider === "local");
|
|
1296
|
+
registerRuntimeModels(models);
|
|
1297
|
+
log("INFO", "app-sidecar", "local model scan", {
|
|
1298
|
+
reachable: probes.filter((p) => p.reachable).length + "/" + probes.length,
|
|
1299
|
+
models: String(models.length),
|
|
1300
|
+
});
|
|
1301
|
+
return probes;
|
|
1302
|
+
}
|
|
1303
|
+
/** Endpoint rows + models, in the shape the Local models UI renders. */
|
|
1304
|
+
function localStatePayload() {
|
|
1305
|
+
return {
|
|
1306
|
+
endpoints: localProbes.map((probe) => ({
|
|
1307
|
+
id: probe.endpoint.id,
|
|
1308
|
+
label: probe.endpoint.label,
|
|
1309
|
+
baseUrl: probe.endpoint.baseUrl,
|
|
1310
|
+
kind: probe.endpoint.kind,
|
|
1311
|
+
custom: probe.endpoint.custom === true,
|
|
1312
|
+
reachable: probe.reachable,
|
|
1313
|
+
...(probe.reason ? { reason: probe.reason } : {}),
|
|
1314
|
+
models: probe.models.map((m) => ({
|
|
1315
|
+
id: formatLocalModelId(probe.endpoint.id, m.rawId),
|
|
1316
|
+
rawId: m.rawId,
|
|
1317
|
+
contextWindow: m.contextWindow,
|
|
1318
|
+
contextWindowKnown: m.contextWindowKnown,
|
|
1319
|
+
supportsTools: m.supportsTools,
|
|
1320
|
+
supportsImages: m.supportsImages,
|
|
1321
|
+
supportsThinking: m.supportsThinking,
|
|
1322
|
+
...(m.loaded === undefined ? {} : { loaded: m.loaded }),
|
|
1323
|
+
})),
|
|
1324
|
+
})),
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Why `modelId` can't be selected right now, or `undefined` when it can.
|
|
1329
|
+
* Two real footguns get a clear answer instead of a mid-run provider error:
|
|
1330
|
+
* a model with no tool calling (can't drive the agent at all), and a server
|
|
1331
|
+
* that has since been shut down.
|
|
1332
|
+
*/
|
|
1333
|
+
async function localModelBlocker(modelId) {
|
|
1334
|
+
const parsed = parseLocalModelId(modelId);
|
|
1335
|
+
if (!parsed)
|
|
1336
|
+
return undefined;
|
|
1337
|
+
const endpoints = await listAllEndpoints();
|
|
1338
|
+
const endpoint = endpoints.find((e) => e.id === parsed.endpointId);
|
|
1339
|
+
if (!endpoint)
|
|
1340
|
+
return `Unknown local endpoint "${parsed.endpointId}" — re-scan for local models.`;
|
|
1341
|
+
const probe = await probeEndpoint(endpoint);
|
|
1342
|
+
// Keep the cached view honest: this probe is fresher than the last scan.
|
|
1343
|
+
localProbes = localProbes.map((p) => (p.endpoint.id === endpoint.id ? probe : p));
|
|
1344
|
+
if (!probe.reachable) {
|
|
1345
|
+
return `${endpoint.label} isn't running at ${endpoint.baseUrl}. Start it and scan again.`;
|
|
1346
|
+
}
|
|
1347
|
+
const model = probe.models.find((m) => m.rawId === parsed.rawId);
|
|
1348
|
+
if (!model) {
|
|
1349
|
+
return `${endpoint.label} no longer serves "${parsed.rawId}".`;
|
|
1350
|
+
}
|
|
1351
|
+
if (!model.supportsTools) {
|
|
1352
|
+
return `${parsed.rawId} has no tool calling, so it can't run the agent. Pick a tool-capable model.`;
|
|
1353
|
+
}
|
|
1354
|
+
registerRuntimeModels(probe.models.map((m) => localModelInfo(m, endpoint)));
|
|
1355
|
+
return undefined;
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* A restored per-project pref can ask for thinking on a local model that
|
|
1359
|
+
* turns out not to reason (capabilities are only known after a probe). Drop
|
|
1360
|
+
* the level once we know, so the first prompt doesn't carry a
|
|
1361
|
+
* `reasoning_effort` the server rejects.
|
|
1362
|
+
*/
|
|
1363
|
+
function clampLocalThinking() {
|
|
1364
|
+
const st = session.getState();
|
|
1365
|
+
const level = session.getThinkingLevel();
|
|
1366
|
+
if (!level || st.provider !== "local")
|
|
1367
|
+
return;
|
|
1368
|
+
if (isThinkingLevelSupported(st.provider, st.model, level))
|
|
1369
|
+
return;
|
|
1370
|
+
session.setThinkingLevel(undefined);
|
|
1371
|
+
broadcast("thinking_change", {
|
|
1372
|
+
thinkingLevel: null,
|
|
1373
|
+
supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1230
1376
|
// Workspace extras (context window, git status, background tasks). Git state
|
|
1231
1377
|
// is resolved once at startup and refreshed after every run; the context
|
|
1232
1378
|
// window follows the active model.
|
|
@@ -1260,8 +1406,23 @@ async function createSession(deps, opts) {
|
|
|
1260
1406
|
gitHubPRs,
|
|
1261
1407
|
gitHubRepoUrl: gitHubSlug ? `https://github.com/${gitHubSlug}` : null,
|
|
1262
1408
|
tasks: session.listBackgroundProcesses(),
|
|
1409
|
+
// Roots added with /add-dir — the header shows a badge when non-empty.
|
|
1410
|
+
additionalRoots: session.getAdditionalRoots(),
|
|
1263
1411
|
};
|
|
1264
1412
|
}
|
|
1413
|
+
void scanLocalModels(false)
|
|
1414
|
+
.then(() => {
|
|
1415
|
+
clampLocalThinking();
|
|
1416
|
+
broadcast("extras", footerExtras());
|
|
1417
|
+
})
|
|
1418
|
+
.then(() => broadcast("models_change", { local: localStatePayload() }))
|
|
1419
|
+
.catch((err) => {
|
|
1420
|
+
// A discovery failure is never fatal — the user simply has no local
|
|
1421
|
+
// models. Log it; don't push an error row into the transcript.
|
|
1422
|
+
log("WARN", "app-sidecar", "local model scan failed", {
|
|
1423
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1424
|
+
});
|
|
1425
|
+
});
|
|
1265
1426
|
// Refresh the GitHub counts and broadcast only on change. Transient failures
|
|
1266
1427
|
// keep the last-known numbers so the chips don't flicker off on a timeout.
|
|
1267
1428
|
async function refreshGitHubCounts() {
|
|
@@ -2430,6 +2591,30 @@ async function createSession(deps, opts) {
|
|
|
2430
2591
|
});
|
|
2431
2592
|
return;
|
|
2432
2593
|
}
|
|
2594
|
+
// Markdown transcript export for the app's download button. Serialized
|
|
2595
|
+
// here rather than in the webview because the webview's transcript model
|
|
2596
|
+
// deliberately keeps tool activity in the LiveToolPanel — exporting from
|
|
2597
|
+
// there would hand the user a coding session with the coding missing.
|
|
2598
|
+
// `?name=1` asks for the suggested filename only (the save dialog needs it
|
|
2599
|
+
// before there is a path), so the markdown never crosses IPC twice.
|
|
2600
|
+
if (method === "GET" && (url === "/export" || url.startsWith("/export?"))) {
|
|
2601
|
+
const query = new URLSearchParams(url.slice(url.indexOf("?") + 1));
|
|
2602
|
+
const st = session.getState();
|
|
2603
|
+
const filename = defaultExportFilename(mode);
|
|
2604
|
+
if (query.get("name") === "1") {
|
|
2605
|
+
json(res, 200, { filename });
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
const markdown = sessionToMarkdown({
|
|
2609
|
+
mode,
|
|
2610
|
+
cwd,
|
|
2611
|
+
provider: st.provider,
|
|
2612
|
+
model: st.model,
|
|
2613
|
+
...(st.sessionId ? { sessionId: st.sessionId } : {}),
|
|
2614
|
+
}, session.getMessages(), { toolDetail: parseToolDetail(query.get("tools")) });
|
|
2615
|
+
json(res, 200, { filename, markdown });
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2433
2618
|
if (method === "GET" && url === "/history") {
|
|
2434
2619
|
// Reconstruct the transcript from persisted messages so resume is 1:1 with
|
|
2435
2620
|
// the live SSE stream. Walks ALL message types (not just user/assistant):
|
|
@@ -2748,16 +2933,34 @@ async function createSession(deps, opts) {
|
|
|
2748
2933
|
description: c.description,
|
|
2749
2934
|
source: "built-in",
|
|
2750
2935
|
}));
|
|
2936
|
+
// Desktop-safe registry actions belong in the same picker as workflows.
|
|
2937
|
+
// Most registry commands have dedicated app controls or TUI-only flows;
|
|
2938
|
+
// multi-root management has no other affordance, so expose only these.
|
|
2939
|
+
const workspaceActions = [
|
|
2940
|
+
{
|
|
2941
|
+
name: "add-dir",
|
|
2942
|
+
aliases: ["adddir"],
|
|
2943
|
+
description: "Add another project folder to this workspace",
|
|
2944
|
+
source: "built-in",
|
|
2945
|
+
},
|
|
2946
|
+
{
|
|
2947
|
+
name: "remove-dir",
|
|
2948
|
+
aliases: ["removedir"],
|
|
2949
|
+
description: "Remove an added project folder from this workspace",
|
|
2950
|
+
source: "built-in",
|
|
2951
|
+
},
|
|
2952
|
+
];
|
|
2751
2953
|
const custom = (await loadCustomCommands(cwd))
|
|
2752
|
-
// A custom command can't shadow a built-in name.
|
|
2753
|
-
.filter((c) => !PROMPT_COMMANDS.some((b) => b.name === c.name)
|
|
2954
|
+
// A custom command can't shadow a built-in name or app action.
|
|
2955
|
+
.filter((c) => !PROMPT_COMMANDS.some((b) => b.name === c.name) &&
|
|
2956
|
+
!workspaceActions.some((action) => action.name === c.name))
|
|
2754
2957
|
.map((c) => ({
|
|
2755
2958
|
name: c.name,
|
|
2756
2959
|
aliases: [],
|
|
2757
2960
|
description: c.description,
|
|
2758
2961
|
source: "custom",
|
|
2759
2962
|
}));
|
|
2760
|
-
json(res, 200, { commands: [...builtins, ...custom] });
|
|
2963
|
+
json(res, 200, { commands: [...workspaceActions, ...builtins, ...custom] });
|
|
2761
2964
|
})();
|
|
2762
2965
|
return;
|
|
2763
2966
|
}
|
|
@@ -3130,12 +3333,30 @@ async function createSession(deps, opts) {
|
|
|
3130
3333
|
loggedIn.push(p);
|
|
3131
3334
|
}
|
|
3132
3335
|
// Just the names, grouped by provider in registry order — the UI shows
|
|
3133
|
-
// a clean multi-column list of model ids.
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
provider
|
|
3138
|
-
|
|
3336
|
+
// a clean multi-column list of model ids. Local models come from the
|
|
3337
|
+
// runtime registry (populated by the background scan) and are always
|
|
3338
|
+
// listed: their "login" is the endpoint answering a probe.
|
|
3339
|
+
const models = getAllModels()
|
|
3340
|
+
.filter((m) => m.provider === "local" || loggedIn.includes(m.provider))
|
|
3341
|
+
.map((m) => {
|
|
3342
|
+
if (m.provider !== "local") {
|
|
3343
|
+
return { id: m.id, name: m.name, provider: m.provider };
|
|
3344
|
+
}
|
|
3345
|
+
const probed = findProbedModel(localProbes, m.id);
|
|
3346
|
+
return {
|
|
3347
|
+
id: m.id,
|
|
3348
|
+
name: m.name,
|
|
3349
|
+
provider: m.provider,
|
|
3350
|
+
local: true,
|
|
3351
|
+
endpoint: probed?.endpoint.label ?? parseLocalModelId(m.id)?.endpointId,
|
|
3352
|
+
// A local model that can't call tools can't run the agent — the UI
|
|
3353
|
+
// renders it disabled rather than hiding it, so the user learns why.
|
|
3354
|
+
supportsTools: probed?.model.supportsTools ?? true,
|
|
3355
|
+
contextWindow: m.contextWindow,
|
|
3356
|
+
contextWindowKnown: probed?.model.contextWindowKnown ?? false,
|
|
3357
|
+
supportsThinking: m.supportsThinking,
|
|
3358
|
+
};
|
|
3359
|
+
});
|
|
3139
3360
|
json(res, 200, { models });
|
|
3140
3361
|
})();
|
|
3141
3362
|
return;
|
|
@@ -3161,6 +3382,13 @@ async function createSession(deps, opts) {
|
|
|
3161
3382
|
json(res, 409, { error: "cannot switch model while running" });
|
|
3162
3383
|
return;
|
|
3163
3384
|
}
|
|
3385
|
+
if (target.provider === "local") {
|
|
3386
|
+
const problem = await localModelBlocker(target.id);
|
|
3387
|
+
if (problem) {
|
|
3388
|
+
json(res, 409, { error: problem });
|
|
3389
|
+
return;
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3164
3392
|
await session.switchModel(target.provider, target.id);
|
|
3165
3393
|
// Ken follows GG Coder's model only while un-pinned; a user-set Ken
|
|
3166
3394
|
// override survives GG model switches untouched.
|
|
@@ -3587,6 +3815,84 @@ async function createSession(deps, opts) {
|
|
|
3587
3815
|
return;
|
|
3588
3816
|
}
|
|
3589
3817
|
// ── Telegram config (mirrors `ggcoder telegram`) ─────────
|
|
3818
|
+
// ── Local models ──────────────────────────────────────
|
|
3819
|
+
// GET returns the last scan (cheap, no probing) so opening the modal is
|
|
3820
|
+
// instant; POST /local/scan is the explicit refresh.
|
|
3821
|
+
if (method === "GET" && url === "/local") {
|
|
3822
|
+
json(res, 200, localStatePayload());
|
|
3823
|
+
return;
|
|
3824
|
+
}
|
|
3825
|
+
if (method === "POST" && url === "/local/scan") {
|
|
3826
|
+
void scanLocalModels(true)
|
|
3827
|
+
.then(() => {
|
|
3828
|
+
broadcast("models_change", { local: localStatePayload() });
|
|
3829
|
+
json(res, 200, localStatePayload());
|
|
3830
|
+
})
|
|
3831
|
+
.catch((err) => {
|
|
3832
|
+
broadcastError("error", "local model scan failed", err);
|
|
3833
|
+
json(res, 500, { error: "Local model scan failed — see the sidecar log." });
|
|
3834
|
+
});
|
|
3835
|
+
return;
|
|
3836
|
+
}
|
|
3837
|
+
if (method === "POST" && url === "/local/endpoints") {
|
|
3838
|
+
void readBody(req, res).then(async (raw) => {
|
|
3839
|
+
if (raw === null)
|
|
3840
|
+
return;
|
|
3841
|
+
let body;
|
|
3842
|
+
try {
|
|
3843
|
+
body = JSON.parse(raw);
|
|
3844
|
+
}
|
|
3845
|
+
catch {
|
|
3846
|
+
json(res, 400, { error: "invalid JSON body" });
|
|
3847
|
+
return;
|
|
3848
|
+
}
|
|
3849
|
+
try {
|
|
3850
|
+
const endpoint = await addCustomEndpoint({
|
|
3851
|
+
baseUrl: body.baseUrl ?? "",
|
|
3852
|
+
...(body.label ? { label: body.label } : {}),
|
|
3853
|
+
...(body.apiKey ? { apiKey: body.apiKey } : {}),
|
|
3854
|
+
});
|
|
3855
|
+
await scanLocalModels(true);
|
|
3856
|
+
broadcast("models_change", { local: localStatePayload() });
|
|
3857
|
+
json(res, 200, {
|
|
3858
|
+
endpoint: { id: endpoint.id, label: endpoint.label },
|
|
3859
|
+
...localStatePayload(),
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3862
|
+
catch (err) {
|
|
3863
|
+
// Validation errors are the user's typo, not a system fault — 400 with
|
|
3864
|
+
// the exact reason, and nothing in the transcript.
|
|
3865
|
+
if (err instanceof LocalEndpointError) {
|
|
3866
|
+
json(res, 400, { error: err.message });
|
|
3867
|
+
return;
|
|
3868
|
+
}
|
|
3869
|
+
broadcastError("error", "add local endpoint failed", err);
|
|
3870
|
+
json(res, 500, { error: "Could not save the endpoint — see the sidecar log." });
|
|
3871
|
+
}
|
|
3872
|
+
});
|
|
3873
|
+
return;
|
|
3874
|
+
}
|
|
3875
|
+
if (method === "DELETE" && url.startsWith("/local/endpoints/")) {
|
|
3876
|
+
const id = decodeURIComponent(url.slice("/local/endpoints/".length));
|
|
3877
|
+
void (async () => {
|
|
3878
|
+
try {
|
|
3879
|
+
await removeCustomEndpoint(id);
|
|
3880
|
+
clearRuntimeModels((m) => m.provider === "local" && parseLocalModelId(m.id)?.endpointId === id);
|
|
3881
|
+
await scanLocalModels(true);
|
|
3882
|
+
broadcast("models_change", { local: localStatePayload() });
|
|
3883
|
+
json(res, 200, localStatePayload());
|
|
3884
|
+
}
|
|
3885
|
+
catch (err) {
|
|
3886
|
+
if (err instanceof LocalEndpointError) {
|
|
3887
|
+
json(res, 400, { error: err.message });
|
|
3888
|
+
return;
|
|
3889
|
+
}
|
|
3890
|
+
broadcastError("error", "remove local endpoint failed", err);
|
|
3891
|
+
json(res, 500, { error: "Could not remove the endpoint — see the sidecar log." });
|
|
3892
|
+
}
|
|
3893
|
+
})();
|
|
3894
|
+
return;
|
|
3895
|
+
}
|
|
3590
3896
|
if (method === "GET" && url === "/telegram") {
|
|
3591
3897
|
void loadTelegramConfig().then((cfg) => {
|
|
3592
3898
|
if (!cfg) {
|