@kenkaiiii/ggcoder 5.24.0 → 5.26.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 +330 -12
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/config.test.js +12 -0
- package/dist/config.test.js.map +1 -1
- package/dist/core/agent-notifications.d.ts +62 -0
- package/dist/core/agent-notifications.d.ts.map +1 -0
- package/dist/core/agent-notifications.js +92 -0
- package/dist/core/agent-notifications.js.map +1 -0
- package/dist/core/agent-notifications.test.d.ts +2 -0
- package/dist/core/agent-notifications.test.d.ts.map +1 -0
- package/dist/core/agent-notifications.test.js +97 -0
- package/dist/core/agent-notifications.test.js.map +1 -0
- package/dist/core/agent-session-add-dir.test.js +16 -0
- package/dist/core/agent-session-add-dir.test.js.map +1 -1
- package/dist/core/agent-session-model-switch.test.d.ts +2 -0
- package/dist/core/agent-session-model-switch.test.d.ts.map +1 -0
- package/dist/core/agent-session-model-switch.test.js +161 -0
- package/dist/core/agent-session-model-switch.test.js.map +1 -0
- package/dist/core/agent-session-notifications.test.d.ts +2 -0
- package/dist/core/agent-session-notifications.test.d.ts.map +1 -0
- package/dist/core/agent-session-notifications.test.js +114 -0
- package/dist/core/agent-session-notifications.test.js.map +1 -0
- package/dist/core/agent-session.d.ts +76 -6
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +246 -25
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/event-bus.d.ts +6 -0
- package/dist/core/event-bus.d.ts.map +1 -1
- package/dist/core/event-bus.js +7 -0
- package/dist/core/event-bus.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/memory/compaction-notes.d.ts +31 -0
- package/dist/core/memory/compaction-notes.d.ts.map +1 -0
- package/dist/core/memory/compaction-notes.js +162 -0
- package/dist/core/memory/compaction-notes.js.map +1 -0
- package/dist/core/memory/compaction-notes.test.d.ts +2 -0
- package/dist/core/memory/compaction-notes.test.d.ts.map +1 -0
- package/dist/core/memory/compaction-notes.test.js +110 -0
- package/dist/core/memory/compaction-notes.test.js.map +1 -0
- package/dist/core/memory/journal.d.ts +55 -0
- package/dist/core/memory/journal.d.ts.map +1 -0
- package/dist/core/memory/journal.js +212 -0
- package/dist/core/memory/journal.js.map +1 -0
- package/dist/core/memory/journal.test.d.ts +2 -0
- package/dist/core/memory/journal.test.d.ts.map +1 -0
- package/dist/core/memory/journal.test.js +184 -0
- package/dist/core/memory/journal.test.js.map +1 -0
- package/dist/core/process-manager-notifications.test.d.ts +2 -0
- package/dist/core/process-manager-notifications.test.d.ts.map +1 -0
- package/dist/core/process-manager-notifications.test.js +113 -0
- package/dist/core/process-manager-notifications.test.js.map +1 -0
- package/dist/core/process-manager.d.ts +29 -0
- package/dist/core/process-manager.d.ts.map +1 -1
- package/dist/core/process-manager.js +123 -0
- package/dist/core/process-manager.js.map +1 -1
- package/dist/core/session-manager.d.ts +3 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +2 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +7 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts +8 -0
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +17 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/slash-commands.test.js +11 -0
- package/dist/core/slash-commands.test.js.map +1 -1
- package/dist/core/steering.d.ts +12 -0
- package/dist/core/steering.d.ts.map +1 -1
- package/dist/core/steering.js +16 -0
- package/dist/core/steering.js.map +1 -1
- package/dist/core/subagent-manager.d.ts +13 -0
- package/dist/core/subagent-manager.d.ts.map +1 -1
- package/dist/core/subagent-manager.js +30 -0
- package/dist/core/subagent-manager.js.map +1 -1
- package/dist/core/subagent-manager.test.js +16 -0
- package/dist/core/subagent-manager.test.js.map +1 -1
- package/dist/modes/json-mode.d.ts.map +1 -1
- package/dist/modes/json-mode.js +5 -0
- package/dist/modes/json-mode.js.map +1 -1
- package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
- package/dist/modes/subagent-worker-mode.js +3 -1
- package/dist/modes/subagent-worker-mode.js.map +1 -1
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/subagent-control.d.ts.map +1 -1
- package/dist/tools/subagent-control.js +6 -2
- package/dist/tools/subagent-control.js.map +1 -1
- package/dist/tools/subagent-shared.d.ts +6 -0
- package/dist/tools/subagent-shared.d.ts.map +1 -1
- package/dist/tools/subagent-shared.js +6 -0
- package/dist/tools/subagent-shared.js.map +1 -1
- package/dist/tools/task-output.d.ts.map +1 -1
- package/dist/tools/task-output.js +3 -1
- package/dist/tools/task-output.js.map +1 -1
- package/dist/ui/App.d.ts +2 -0
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +35 -1
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/render.d.ts +2 -0
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +1 -0
- package/dist/ui/render.js.map +1 -1
- package/package.json +4 -4
package/dist/app-sidecar.js
CHANGED
|
@@ -38,7 +38,8 @@ import { sessionToMarkdown, defaultExportFilename, } from "./core/session-export
|
|
|
38
38
|
import { AuthStorage } from "./core/auth-storage.js";
|
|
39
39
|
import { cleanupToolOutputs } from "./tools/overflow.js";
|
|
40
40
|
import { readCappedBody } from "./utils/http-body.js";
|
|
41
|
-
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";
|
|
42
43
|
import { loginAnthropic } from "./core/oauth/anthropic.js";
|
|
43
44
|
import { loginOpenAI } from "./core/oauth/openai.js";
|
|
44
45
|
import { loginGemini } from "./core/oauth/gemini.js";
|
|
@@ -46,7 +47,7 @@ import { loginKimi } from "./core/oauth/kimi.js";
|
|
|
46
47
|
import { AUTH_PROVIDERS } from "./core/auth-providers.js";
|
|
47
48
|
import { ensureAppDirs, loadSavedSettings } from "./config.js";
|
|
48
49
|
import { SettingsManager } from "./core/settings-manager.js";
|
|
49
|
-
import { getModel, getDefaultThinkingLevel, getContextWindow,
|
|
50
|
+
import { getModel, getDefaultThinkingLevel, getContextWindow, getAllModels, clearRuntimeModels, registerRuntimeModels, } from "./core/model-registry.js";
|
|
50
51
|
import { resolveStartOrFallback } from "./core/resolve-start.js";
|
|
51
52
|
import { getGitBranch, getGitDirtyFileCount, isGitRepo } from "./utils/git.js";
|
|
52
53
|
import { getGitHubOpenCounts, getGitHubRepoSlug } from "./utils/github.js";
|
|
@@ -620,6 +621,13 @@ async function main() {
|
|
|
620
621
|
});
|
|
621
622
|
const usageCache = new Map();
|
|
622
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;
|
|
623
631
|
// 429 backoff: quota endpoints are auxiliary UI data. Honor Retry-After when
|
|
624
632
|
// provided; otherwise retain the unavailable snapshot for 30 minutes. Clamp
|
|
625
633
|
// the provider value so a malformed header can neither hammer the endpoint
|
|
@@ -634,6 +642,9 @@ async function main() {
|
|
|
634
642
|
// Moonshot platform API key is metered per-token, not per plan window.
|
|
635
643
|
const authKey = provider === "moonshot" ? MOONSHOT_OAUTH_KEY : provider;
|
|
636
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);
|
|
637
648
|
return { provider, displayName, connected: false, windows: [], fetchedAt: Date.now() };
|
|
638
649
|
}
|
|
639
650
|
try {
|
|
@@ -677,6 +688,18 @@ async function main() {
|
|
|
677
688
|
...(backoffMs !== undefined && { backoffMs: String(backoffMs) }),
|
|
678
689
|
});
|
|
679
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
|
+
}
|
|
680
703
|
return {
|
|
681
704
|
provider,
|
|
682
705
|
displayName,
|
|
@@ -698,6 +721,11 @@ async function main() {
|
|
|
698
721
|
usageRequests.set(provider, request);
|
|
699
722
|
try {
|
|
700
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
|
+
}
|
|
701
729
|
// Anthropic can return utilization before it assigns reset timestamps
|
|
702
730
|
// (notably before the account's first active request). Retry that partial
|
|
703
731
|
// snapshot quickly; complete snapshots keep the normal one-minute cache.
|
|
@@ -1116,6 +1144,25 @@ async function createSession(deps, opts) {
|
|
|
1116
1144
|
// /help
|
|
1117
1145
|
.replaceAll(/see \/help/gi, "check the help menu"));
|
|
1118
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
|
+
}
|
|
1119
1166
|
// Turn any thrown value into the same clear headline/message/guidance shape
|
|
1120
1167
|
// the TUI shows (see gg-ai's formatError) instead of a bare `err.message`, log
|
|
1121
1168
|
// the full detail, and broadcast it under `type` ("error" or "ken_error").
|
|
@@ -1125,7 +1172,7 @@ async function createSession(deps, opts) {
|
|
|
1125
1172
|
function broadcastError(type, logLabel, err) {
|
|
1126
1173
|
const f = formatError(err);
|
|
1127
1174
|
const message = f.message ? desktopGuidance(f.message) : undefined;
|
|
1128
|
-
const guidance = desktopGuidance(f.guidance);
|
|
1175
|
+
const guidance = localNetworkGuidance(f.source) ?? desktopGuidance(f.guidance);
|
|
1129
1176
|
captureSidecarError(err, `app-sidecar.${logLabel.replaceAll(" ", "-")}`, {
|
|
1130
1177
|
scope: type,
|
|
1131
1178
|
...(f.provider ? { provider: f.provider } : {}),
|
|
@@ -1233,6 +1280,99 @@ async function createSession(deps, opts) {
|
|
|
1233
1280
|
}
|
|
1234
1281
|
}
|
|
1235
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
|
+
}
|
|
1236
1376
|
// Workspace extras (context window, git status, background tasks). Git state
|
|
1237
1377
|
// is resolved once at startup and refreshed after every run; the context
|
|
1238
1378
|
// window follows the active model.
|
|
@@ -1270,6 +1410,19 @@ async function createSession(deps, opts) {
|
|
|
1270
1410
|
additionalRoots: session.getAdditionalRoots(),
|
|
1271
1411
|
};
|
|
1272
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
|
+
});
|
|
1273
1426
|
// Refresh the GitHub counts and broadcast only on change. Transient failures
|
|
1274
1427
|
// keep the last-known numbers so the chips don't flicker off on a timeout.
|
|
1275
1428
|
async function refreshGitHubCounts() {
|
|
@@ -2249,6 +2402,50 @@ async function createSession(deps, opts) {
|
|
|
2249
2402
|
});
|
|
2250
2403
|
return;
|
|
2251
2404
|
}
|
|
2405
|
+
// Durable per-project journal (.gg/memory.md). Distinct from /memories,
|
|
2406
|
+
// which is the personal chat-memory store shared by the chat agents.
|
|
2407
|
+
if (method === "GET" && url === "/project-memory") {
|
|
2408
|
+
void (async () => {
|
|
2409
|
+
try {
|
|
2410
|
+
const sm = new SettingsManager(paths.settingsFile);
|
|
2411
|
+
await sm.load();
|
|
2412
|
+
json(res, 200, { enabled: sm.get("memoryEnabled") });
|
|
2413
|
+
}
|
|
2414
|
+
catch (error) {
|
|
2415
|
+
captureSidecarError(error, "app-sidecar.project-memory.get");
|
|
2416
|
+
json(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2417
|
+
}
|
|
2418
|
+
})();
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
if (method === "POST" && url === "/project-memory") {
|
|
2422
|
+
void readBody(req, res).then(async (raw) => {
|
|
2423
|
+
if (raw === null)
|
|
2424
|
+
return;
|
|
2425
|
+
let enabled;
|
|
2426
|
+
try {
|
|
2427
|
+
enabled = JSON.parse(raw).enabled === true;
|
|
2428
|
+
}
|
|
2429
|
+
catch {
|
|
2430
|
+
json(res, 400, { error: "invalid JSON body" });
|
|
2431
|
+
return;
|
|
2432
|
+
}
|
|
2433
|
+
try {
|
|
2434
|
+
const sm = new SettingsManager(paths.settingsFile);
|
|
2435
|
+
await sm.load();
|
|
2436
|
+
await sm.set("memoryEnabled", enabled);
|
|
2437
|
+
// Applies from the next session: the journal handle is built during
|
|
2438
|
+
// AgentSession.initialize(), so a live run keeps its current behaviour
|
|
2439
|
+
// rather than half-applying the change mid-task.
|
|
2440
|
+
json(res, 200, { enabled });
|
|
2441
|
+
}
|
|
2442
|
+
catch (error) {
|
|
2443
|
+
captureSidecarError(error, "app-sidecar.project-memory.set");
|
|
2444
|
+
json(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2445
|
+
}
|
|
2446
|
+
});
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2252
2449
|
if (method === "GET" && url === "/jiwa") {
|
|
2253
2450
|
void jiwaStore
|
|
2254
2451
|
.snapshot()
|
|
@@ -2780,16 +2977,34 @@ async function createSession(deps, opts) {
|
|
|
2780
2977
|
description: c.description,
|
|
2781
2978
|
source: "built-in",
|
|
2782
2979
|
}));
|
|
2980
|
+
// Desktop-safe registry actions belong in the same picker as workflows.
|
|
2981
|
+
// Most registry commands have dedicated app controls or TUI-only flows;
|
|
2982
|
+
// multi-root management has no other affordance, so expose only these.
|
|
2983
|
+
const workspaceActions = [
|
|
2984
|
+
{
|
|
2985
|
+
name: "add-dir",
|
|
2986
|
+
aliases: ["adddir"],
|
|
2987
|
+
description: "Add another project folder to this workspace",
|
|
2988
|
+
source: "built-in",
|
|
2989
|
+
},
|
|
2990
|
+
{
|
|
2991
|
+
name: "remove-dir",
|
|
2992
|
+
aliases: ["removedir"],
|
|
2993
|
+
description: "Remove an added project folder from this workspace",
|
|
2994
|
+
source: "built-in",
|
|
2995
|
+
},
|
|
2996
|
+
];
|
|
2783
2997
|
const custom = (await loadCustomCommands(cwd))
|
|
2784
|
-
// A custom command can't shadow a built-in name.
|
|
2785
|
-
.filter((c) => !PROMPT_COMMANDS.some((b) => b.name === c.name)
|
|
2998
|
+
// A custom command can't shadow a built-in name or app action.
|
|
2999
|
+
.filter((c) => !PROMPT_COMMANDS.some((b) => b.name === c.name) &&
|
|
3000
|
+
!workspaceActions.some((action) => action.name === c.name))
|
|
2786
3001
|
.map((c) => ({
|
|
2787
3002
|
name: c.name,
|
|
2788
3003
|
aliases: [],
|
|
2789
3004
|
description: c.description,
|
|
2790
3005
|
source: "custom",
|
|
2791
3006
|
}));
|
|
2792
|
-
json(res, 200, { commands: [...builtins, ...custom] });
|
|
3007
|
+
json(res, 200, { commands: [...workspaceActions, ...builtins, ...custom] });
|
|
2793
3008
|
})();
|
|
2794
3009
|
return;
|
|
2795
3010
|
}
|
|
@@ -3162,12 +3377,30 @@ async function createSession(deps, opts) {
|
|
|
3162
3377
|
loggedIn.push(p);
|
|
3163
3378
|
}
|
|
3164
3379
|
// Just the names, grouped by provider in registry order — the UI shows
|
|
3165
|
-
// a clean multi-column list of model ids.
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
provider
|
|
3170
|
-
|
|
3380
|
+
// a clean multi-column list of model ids. Local models come from the
|
|
3381
|
+
// runtime registry (populated by the background scan) and are always
|
|
3382
|
+
// listed: their "login" is the endpoint answering a probe.
|
|
3383
|
+
const models = getAllModels()
|
|
3384
|
+
.filter((m) => m.provider === "local" || loggedIn.includes(m.provider))
|
|
3385
|
+
.map((m) => {
|
|
3386
|
+
if (m.provider !== "local") {
|
|
3387
|
+
return { id: m.id, name: m.name, provider: m.provider };
|
|
3388
|
+
}
|
|
3389
|
+
const probed = findProbedModel(localProbes, m.id);
|
|
3390
|
+
return {
|
|
3391
|
+
id: m.id,
|
|
3392
|
+
name: m.name,
|
|
3393
|
+
provider: m.provider,
|
|
3394
|
+
local: true,
|
|
3395
|
+
endpoint: probed?.endpoint.label ?? parseLocalModelId(m.id)?.endpointId,
|
|
3396
|
+
// A local model that can't call tools can't run the agent — the UI
|
|
3397
|
+
// renders it disabled rather than hiding it, so the user learns why.
|
|
3398
|
+
supportsTools: probed?.model.supportsTools ?? true,
|
|
3399
|
+
contextWindow: m.contextWindow,
|
|
3400
|
+
contextWindowKnown: probed?.model.contextWindowKnown ?? false,
|
|
3401
|
+
supportsThinking: m.supportsThinking,
|
|
3402
|
+
};
|
|
3403
|
+
});
|
|
3171
3404
|
json(res, 200, { models });
|
|
3172
3405
|
})();
|
|
3173
3406
|
return;
|
|
@@ -3193,6 +3426,13 @@ async function createSession(deps, opts) {
|
|
|
3193
3426
|
json(res, 409, { error: "cannot switch model while running" });
|
|
3194
3427
|
return;
|
|
3195
3428
|
}
|
|
3429
|
+
if (target.provider === "local") {
|
|
3430
|
+
const problem = await localModelBlocker(target.id);
|
|
3431
|
+
if (problem) {
|
|
3432
|
+
json(res, 409, { error: problem });
|
|
3433
|
+
return;
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3196
3436
|
await session.switchModel(target.provider, target.id);
|
|
3197
3437
|
// Ken follows GG Coder's model only while un-pinned; a user-set Ken
|
|
3198
3438
|
// override survives GG model switches untouched.
|
|
@@ -3619,6 +3859,84 @@ async function createSession(deps, opts) {
|
|
|
3619
3859
|
return;
|
|
3620
3860
|
}
|
|
3621
3861
|
// ── Telegram config (mirrors `ggcoder telegram`) ─────────
|
|
3862
|
+
// ── Local models ──────────────────────────────────────
|
|
3863
|
+
// GET returns the last scan (cheap, no probing) so opening the modal is
|
|
3864
|
+
// instant; POST /local/scan is the explicit refresh.
|
|
3865
|
+
if (method === "GET" && url === "/local") {
|
|
3866
|
+
json(res, 200, localStatePayload());
|
|
3867
|
+
return;
|
|
3868
|
+
}
|
|
3869
|
+
if (method === "POST" && url === "/local/scan") {
|
|
3870
|
+
void scanLocalModels(true)
|
|
3871
|
+
.then(() => {
|
|
3872
|
+
broadcast("models_change", { local: localStatePayload() });
|
|
3873
|
+
json(res, 200, localStatePayload());
|
|
3874
|
+
})
|
|
3875
|
+
.catch((err) => {
|
|
3876
|
+
broadcastError("error", "local model scan failed", err);
|
|
3877
|
+
json(res, 500, { error: "Local model scan failed — see the sidecar log." });
|
|
3878
|
+
});
|
|
3879
|
+
return;
|
|
3880
|
+
}
|
|
3881
|
+
if (method === "POST" && url === "/local/endpoints") {
|
|
3882
|
+
void readBody(req, res).then(async (raw) => {
|
|
3883
|
+
if (raw === null)
|
|
3884
|
+
return;
|
|
3885
|
+
let body;
|
|
3886
|
+
try {
|
|
3887
|
+
body = JSON.parse(raw);
|
|
3888
|
+
}
|
|
3889
|
+
catch {
|
|
3890
|
+
json(res, 400, { error: "invalid JSON body" });
|
|
3891
|
+
return;
|
|
3892
|
+
}
|
|
3893
|
+
try {
|
|
3894
|
+
const endpoint = await addCustomEndpoint({
|
|
3895
|
+
baseUrl: body.baseUrl ?? "",
|
|
3896
|
+
...(body.label ? { label: body.label } : {}),
|
|
3897
|
+
...(body.apiKey ? { apiKey: body.apiKey } : {}),
|
|
3898
|
+
});
|
|
3899
|
+
await scanLocalModels(true);
|
|
3900
|
+
broadcast("models_change", { local: localStatePayload() });
|
|
3901
|
+
json(res, 200, {
|
|
3902
|
+
endpoint: { id: endpoint.id, label: endpoint.label },
|
|
3903
|
+
...localStatePayload(),
|
|
3904
|
+
});
|
|
3905
|
+
}
|
|
3906
|
+
catch (err) {
|
|
3907
|
+
// Validation errors are the user's typo, not a system fault — 400 with
|
|
3908
|
+
// the exact reason, and nothing in the transcript.
|
|
3909
|
+
if (err instanceof LocalEndpointError) {
|
|
3910
|
+
json(res, 400, { error: err.message });
|
|
3911
|
+
return;
|
|
3912
|
+
}
|
|
3913
|
+
broadcastError("error", "add local endpoint failed", err);
|
|
3914
|
+
json(res, 500, { error: "Could not save the endpoint — see the sidecar log." });
|
|
3915
|
+
}
|
|
3916
|
+
});
|
|
3917
|
+
return;
|
|
3918
|
+
}
|
|
3919
|
+
if (method === "DELETE" && url.startsWith("/local/endpoints/")) {
|
|
3920
|
+
const id = decodeURIComponent(url.slice("/local/endpoints/".length));
|
|
3921
|
+
void (async () => {
|
|
3922
|
+
try {
|
|
3923
|
+
await removeCustomEndpoint(id);
|
|
3924
|
+
clearRuntimeModels((m) => m.provider === "local" && parseLocalModelId(m.id)?.endpointId === id);
|
|
3925
|
+
await scanLocalModels(true);
|
|
3926
|
+
broadcast("models_change", { local: localStatePayload() });
|
|
3927
|
+
json(res, 200, localStatePayload());
|
|
3928
|
+
}
|
|
3929
|
+
catch (err) {
|
|
3930
|
+
if (err instanceof LocalEndpointError) {
|
|
3931
|
+
json(res, 400, { error: err.message });
|
|
3932
|
+
return;
|
|
3933
|
+
}
|
|
3934
|
+
broadcastError("error", "remove local endpoint failed", err);
|
|
3935
|
+
json(res, 500, { error: "Could not remove the endpoint — see the sidecar log." });
|
|
3936
|
+
}
|
|
3937
|
+
})();
|
|
3938
|
+
return;
|
|
3939
|
+
}
|
|
3622
3940
|
if (method === "GET" && url === "/telegram") {
|
|
3623
3941
|
void loadTelegramConfig().then((cfg) => {
|
|
3624
3942
|
if (!cfg) {
|