codeep 2.13.0 → 2.13.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/README.md +9 -1
- package/dist/acp/server.d.ts +41 -0
- package/dist/acp/server.js +36 -155
- package/dist/acp/serverHandlers.d.ts +88 -0
- package/dist/acp/serverHandlers.js +237 -0
- package/dist/acp/session.d.ts +9 -0
- package/dist/acp/session.js +6 -2
- package/dist/config/index.d.ts +2 -2
- package/dist/renderer/App.js +6 -119
- package/dist/renderer/commands/registry.d.ts +92 -0
- package/dist/renderer/commands/registry.js +488 -0
- package/dist/renderer/components/Help.d.ts +12 -3
- package/dist/renderer/components/Help.js +14 -176
- package/dist/renderer/components/Settings.d.ts +5 -1
- package/dist/renderer/components/Settings.js +23 -5
- package/dist/utils/gitHookInstaller.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -10
package/README.md
CHANGED
|
@@ -1340,7 +1340,15 @@ Apache 2.0
|
|
|
1340
1340
|
|
|
1341
1341
|
## Contributing
|
|
1342
1342
|
|
|
1343
|
-
Contributions are welcome!
|
|
1343
|
+
Contributions are welcome! Before opening a PR, please read the
|
|
1344
|
+
[**Contributing guide**](./CONTRIBUTING.md) — it covers the development setup
|
|
1345
|
+
(npm is the canonical package manager), the test/build loop, code style, and
|
|
1346
|
+
how to propose a change. For provider integrations specifically, the guide
|
|
1347
|
+
walks through the files you need to touch.
|
|
1348
|
+
|
|
1349
|
+
Quick version: `npm install` → `npm test` → `npx tsc --noEmit` clean → open a
|
|
1350
|
+
PR against `main`. To report a security issue, see
|
|
1351
|
+
[**Security policy**](./SECURITY.md).
|
|
1344
1352
|
|
|
1345
1353
|
## Support
|
|
1346
1354
|
|
package/dist/acp/server.d.ts
CHANGED
|
@@ -1 +1,42 @@
|
|
|
1
|
+
import { SessionModeState, SessionConfigOption } from './protocol.js';
|
|
2
|
+
export declare const AGENT_MODES: SessionModeState;
|
|
3
|
+
/**
|
|
4
|
+
* Format a tool call's parameters into a human-readable object for the
|
|
5
|
+
* permission dialog. Truncates long content fields so the dialog stays readable.
|
|
6
|
+
*
|
|
7
|
+
* Exported for unit testing (see server.test.ts).
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatToolInputForPermission(tool: string, params: Record<string, unknown>): Record<string, string>;
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a `file://` (or absolute path) URI to a local filesystem path.
|
|
12
|
+
* Returns null for unsupported schemes (http/https/git/etc.) — we only embed
|
|
13
|
+
* local content for safety/privacy.
|
|
14
|
+
*
|
|
15
|
+
* Exported for unit testing (see server.test.ts).
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveLocalPath(uri: string): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Walk the prompt's ContentBlock[] looking for ResourceLink and Resource
|
|
20
|
+
* blocks. ResourceLink → read file from disk. Resource → use embedded text.
|
|
21
|
+
* Returns a markdown-fenced snippet block ready to prepend to the prompt,
|
|
22
|
+
* or empty string if there are no embedded contexts.
|
|
23
|
+
*
|
|
24
|
+
* Exported for unit testing (see server.test.ts).
|
|
25
|
+
*/
|
|
26
|
+
export declare function collectEmbeddedContext(blocks: import('./protocol.js').ContentBlock[]): Promise<string>;
|
|
27
|
+
/** Check if a provider has an API key stored (synchronous; relies on the cache
|
|
28
|
+
* loaded at startup via loadAllApiKeys and the non-secret configuredProviderIds
|
|
29
|
+
* index — never reads plaintext key material).
|
|
30
|
+
*
|
|
31
|
+
* Exported for unit testing (see server.test.ts).
|
|
32
|
+
*/
|
|
33
|
+
export declare function providerHasKey(providerId: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Build the list of session-level config options advertised to the ACP client
|
|
36
|
+
* (provider/model picker, language, per-tool confirmation toggles).
|
|
37
|
+
*
|
|
38
|
+
* Reads the global config + provider table; pure of transport/session state.
|
|
39
|
+
* Exported for unit testing (see server.test.ts).
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildConfigOptions(): SessionConfigOption[];
|
|
1
42
|
export declare function startAcpServer(): Promise<void>;
|
package/dist/acp/server.js
CHANGED
|
@@ -7,12 +7,13 @@ import { fileURLToPath } from 'url';
|
|
|
7
7
|
import { StdioTransport } from './transport.js';
|
|
8
8
|
import { runAgentSession } from './session.js';
|
|
9
9
|
import { loadCustomCommands } from '../utils/customCommands.js';
|
|
10
|
-
import { registerSessionServers,
|
|
10
|
+
import { registerSessionServers, disposeAllSessions as disposeAllMcpSessions } from '../utils/mcpRegistry.js';
|
|
11
11
|
import { loadMcpServerConfig, mergeMcpServers } from '../utils/mcpConfig.js';
|
|
12
12
|
import { handleMcpSamplingRequest } from '../utils/mcpSamplingBridge.js';
|
|
13
13
|
import { executeCommandAsync } from '../utils/shell.js';
|
|
14
14
|
import { initWorkspace, loadWorkspace, handleCommand } from './commands.js';
|
|
15
|
-
import {
|
|
15
|
+
import { handleSetMode as handleSetModeExternal, handleSetConfigOption as handleSetConfigOptionExternal, handleSessionList as handleSessionListExternal, handleSessionDelete as handleSessionDeleteExternal, handleListProviders as handleListProvidersExternal, } from './serverHandlers.js';
|
|
16
|
+
import { autoSaveSession, config, getApiKey, getConfiguredProviders } from '../config/index.js';
|
|
16
17
|
import { ApiError } from '../api/index.js';
|
|
17
18
|
import { PROVIDERS } from '../config/providers.js';
|
|
18
19
|
import { getCurrentVersion } from '../utils/update.js';
|
|
@@ -87,7 +88,8 @@ const AVAILABLE_COMMANDS = [
|
|
|
87
88
|
{ name: 'deploy', description: 'Deploy the project' },
|
|
88
89
|
];
|
|
89
90
|
// ─── Mode definitions ─────────────────────────────────────────────────────────
|
|
90
|
-
|
|
91
|
+
// Exported for unit testing + reference from serverHandlers modules.
|
|
92
|
+
export const AGENT_MODES = {
|
|
91
93
|
currentModeId: 'auto',
|
|
92
94
|
availableModes: [
|
|
93
95
|
{ id: 'auto', name: 'Auto', description: 'Agent runs automatically without confirmation' },
|
|
@@ -112,8 +114,10 @@ const LANGUAGE_OPTIONS = [
|
|
|
112
114
|
/**
|
|
113
115
|
* Format a tool call's parameters into a human-readable object for the
|
|
114
116
|
* permission dialog. Truncates long content fields so the dialog stays readable.
|
|
117
|
+
*
|
|
118
|
+
* Exported for unit testing (see server.test.ts).
|
|
115
119
|
*/
|
|
116
|
-
function formatToolInputForPermission(tool, params) {
|
|
120
|
+
export function formatToolInputForPermission(tool, params) {
|
|
117
121
|
const MAX_LEN = 120;
|
|
118
122
|
// Bigger budget for diff/content fields so clients can render an actual preview
|
|
119
123
|
// before the user clicks Allow. Truncated with a visible marker so users know
|
|
@@ -167,8 +171,10 @@ function formatToolInputForPermission(tool, params) {
|
|
|
167
171
|
* Resolve a `file://` (or absolute path) URI to a local filesystem path.
|
|
168
172
|
* Returns null for unsupported schemes (http/https/git/etc.) — we only embed
|
|
169
173
|
* local content for safety/privacy.
|
|
174
|
+
*
|
|
175
|
+
* Exported for unit testing (see server.test.ts).
|
|
170
176
|
*/
|
|
171
|
-
function resolveLocalPath(uri) {
|
|
177
|
+
export function resolveLocalPath(uri) {
|
|
172
178
|
if (!uri)
|
|
173
179
|
return null;
|
|
174
180
|
if (uri.startsWith('file://')) {
|
|
@@ -189,8 +195,10 @@ function resolveLocalPath(uri) {
|
|
|
189
195
|
* blocks. ResourceLink → read file from disk. Resource → use embedded text.
|
|
190
196
|
* Returns a markdown-fenced snippet block ready to prepend to the prompt,
|
|
191
197
|
* or empty string if there are no embedded contexts.
|
|
198
|
+
*
|
|
199
|
+
* Exported for unit testing (see server.test.ts).
|
|
192
200
|
*/
|
|
193
|
-
async function collectEmbeddedContext(blocks) {
|
|
201
|
+
export async function collectEmbeddedContext(blocks) {
|
|
194
202
|
const MAX_BYTES_PER_FILE = 200_000; // ~200 KB cap per resource
|
|
195
203
|
const snippets = [];
|
|
196
204
|
for (const block of blocks) {
|
|
@@ -243,8 +251,11 @@ async function collectEmbeddedContext(blocks) {
|
|
|
243
251
|
}
|
|
244
252
|
/** Check if a provider has an API key stored (synchronous; relies on the cache
|
|
245
253
|
* loaded at startup via loadAllApiKeys and the non-secret configuredProviderIds
|
|
246
|
-
* index — never reads plaintext key material).
|
|
247
|
-
|
|
254
|
+
* index — never reads plaintext key material).
|
|
255
|
+
*
|
|
256
|
+
* Exported for unit testing (see server.test.ts).
|
|
257
|
+
*/
|
|
258
|
+
export function providerHasKey(providerId) {
|
|
248
259
|
// Check environment variable first
|
|
249
260
|
const envKey = PROVIDERS[providerId]?.envKey;
|
|
250
261
|
if (envKey && process.env[envKey])
|
|
@@ -255,7 +266,14 @@ function providerHasKey(providerId) {
|
|
|
255
266
|
// Non-secret index of providers that have a key in secure storage
|
|
256
267
|
return getConfiguredProviders().some(p => p.id === providerId);
|
|
257
268
|
}
|
|
258
|
-
|
|
269
|
+
/**
|
|
270
|
+
* Build the list of session-level config options advertised to the ACP client
|
|
271
|
+
* (provider/model picker, language, per-tool confirmation toggles).
|
|
272
|
+
*
|
|
273
|
+
* Reads the global config + provider table; pure of transport/session state.
|
|
274
|
+
* Exported for unit testing (see server.test.ts).
|
|
275
|
+
*/
|
|
276
|
+
export function buildConfigOptions() {
|
|
259
277
|
const currentModel = config.get('model') ?? '';
|
|
260
278
|
const currentProviderId = config.get('provider') ?? '';
|
|
261
279
|
// Only show providers that have an API key configured
|
|
@@ -345,6 +363,10 @@ export function startAcpServer() {
|
|
|
345
363
|
const transport = new StdioTransport();
|
|
346
364
|
// ACP sessionId → full AcpSession (includes history + codeep session tracking)
|
|
347
365
|
const sessions = new Map();
|
|
366
|
+
// Shared deps object for the extracted handlers in serverHandlers.ts.
|
|
367
|
+
// Handlers read transport + sessions off this; stubbing both in a test
|
|
368
|
+
// (see serverHandlers.test.ts) is what makes them unit-testable.
|
|
369
|
+
const handlerDeps = { transport, sessions };
|
|
348
370
|
// Tear down all MCP child processes when the CLI dies. Without this,
|
|
349
371
|
// killing `codeep acp` with Ctrl+C orphans any servers we spawned —
|
|
350
372
|
// they keep running until the user hunts them down with `ps`.
|
|
@@ -720,143 +742,19 @@ export function startAcpServer() {
|
|
|
720
742
|
}
|
|
721
743
|
// ── session/set_mode ────────────────────────────────────────────────────────
|
|
722
744
|
function handleSetMode(msg) {
|
|
723
|
-
|
|
724
|
-
const session = sessions.get(sessionId);
|
|
725
|
-
if (!session) {
|
|
726
|
-
transport.error(msg.id, -32602, `Unknown sessionId: ${sessionId}`);
|
|
727
|
-
return;
|
|
728
|
-
}
|
|
729
|
-
const validMode = AGENT_MODES.availableModes.find(m => m.id === modeId);
|
|
730
|
-
if (!validMode) {
|
|
731
|
-
transport.error(msg.id, -32602, `Unknown modeId: ${modeId}`);
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
session.currentModeId = modeId;
|
|
735
|
-
// Do NOT persist the global `agentConfirmation` config here. The ACP
|
|
736
|
-
// permission gate is driven per-session by `session.currentModeId` (see the
|
|
737
|
-
// onRequestPermission wiring in handleSessionPrompt). Writing it globally
|
|
738
|
-
// would leak this session's mode into other processes — e.g. switching ACP
|
|
739
|
-
// to 'auto' would silently disarm the TUI's confirmation gate. Mode stays
|
|
740
|
-
// on the in-memory session object only.
|
|
741
|
-
transport.respond(msg.id, {});
|
|
742
|
-
// Notify Zed of the mode change
|
|
743
|
-
transport.notify('session/update', {
|
|
744
|
-
sessionId,
|
|
745
|
-
update: {
|
|
746
|
-
sessionUpdate: 'current_mode_update',
|
|
747
|
-
currentModeId: modeId,
|
|
748
|
-
},
|
|
749
|
-
});
|
|
745
|
+
handleSetModeExternal(msg, handlerDeps);
|
|
750
746
|
}
|
|
751
747
|
// ── session/set_config_option ───────────────────────────────────────────────
|
|
752
748
|
function handleSetConfigOption(msg) {
|
|
753
|
-
|
|
754
|
-
const session = sessions.get(sessionId);
|
|
755
|
-
if (!session) {
|
|
756
|
-
transport.error(msg.id, -32602, `Unknown sessionId: ${sessionId}`);
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
if (configId === 'model' && typeof value === 'string') {
|
|
760
|
-
// value is "providerId/modelId" — split and switch both
|
|
761
|
-
const slashIdx = value.indexOf('/');
|
|
762
|
-
if (slashIdx !== -1) {
|
|
763
|
-
const providerId = value.slice(0, slashIdx);
|
|
764
|
-
const modelId = value.slice(slashIdx + 1);
|
|
765
|
-
setProvider(providerId); // sets provider + defaultModel + protocol
|
|
766
|
-
config.set('model', modelId);
|
|
767
|
-
}
|
|
768
|
-
else {
|
|
769
|
-
config.set('model', value);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
else if (configId === 'provider' && typeof value === 'string') {
|
|
773
|
-
// Switch provider without specifying a model — picks the provider's
|
|
774
|
-
// default model + protocol. Used by editor clients that pin a provider
|
|
775
|
-
// in their settings (e.g. the VS Code `codeep.provider` setting).
|
|
776
|
-
setProvider(value);
|
|
777
|
-
}
|
|
778
|
-
else if (configId === 'customBaseUrl' && typeof value === 'string') {
|
|
779
|
-
// Base URL for the `custom` (OpenAI-compatible) provider — lets editor
|
|
780
|
-
// clients point Codeep at a self-hosted endpoint (vLLM/LiteLLM/LM Studio)
|
|
781
|
-
// without hand-editing ~/.codeep/config.json.
|
|
782
|
-
config.set('customBaseUrl', value);
|
|
783
|
-
}
|
|
784
|
-
else if (configId === 'language' && typeof value === 'string') {
|
|
785
|
-
config.set('language', value);
|
|
786
|
-
}
|
|
787
|
-
else if (configId === 'agentConfirmDeleteFile' ||
|
|
788
|
-
configId === 'agentConfirmExecuteCommand' ||
|
|
789
|
-
configId === 'agentConfirmWriteFile' ||
|
|
790
|
-
configId === 'userProfile' ||
|
|
791
|
-
configId === 'autoLearnProfile') {
|
|
792
|
-
// Accept boolean or "true"/"false" string from Zed/VSCode
|
|
793
|
-
const bool = value === true || value === 'true';
|
|
794
|
-
config.set(configId, bool);
|
|
795
|
-
}
|
|
796
|
-
else if (configId === 'login' && typeof value === 'string') {
|
|
797
|
-
// value is "providerId:apiKey"
|
|
798
|
-
const colonIdx = value.indexOf(':');
|
|
799
|
-
if (colonIdx !== -1) {
|
|
800
|
-
const providerId = value.slice(0, colonIdx);
|
|
801
|
-
const apiKey = value.slice(colonIdx + 1);
|
|
802
|
-
if (providerId && apiKey) {
|
|
803
|
-
setProvider(providerId);
|
|
804
|
-
// Cache is updated synchronously inside setApiKey; the keychain write
|
|
805
|
-
// resolves while the long-lived server runs. Swallow a persistence
|
|
806
|
-
// failure here (secondary path) so it can't become an unhandled
|
|
807
|
-
// rejection — the key still works for this session via the cache.
|
|
808
|
-
setApiKey(apiKey, providerId).catch(() => { });
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
transport.respond(msg.id, {});
|
|
813
|
-
// Confirm the new value back to Zed so its UI state stays in sync
|
|
814
|
-
transport.notify('session/update', {
|
|
815
|
-
sessionId,
|
|
816
|
-
update: {
|
|
817
|
-
sessionUpdate: 'config_option_update',
|
|
818
|
-
configOptions: buildConfigOptions(),
|
|
819
|
-
},
|
|
820
|
-
});
|
|
749
|
+
handleSetConfigOptionExternal(msg, handlerDeps);
|
|
821
750
|
}
|
|
822
751
|
// ── session/list ─────────────────────────────────────────────────────────────
|
|
823
752
|
function handleSessionList(msg) {
|
|
824
|
-
|
|
825
|
-
// Collect local (project-scoped) sessions and global sessions, deduplicated by name
|
|
826
|
-
const seen = new Set();
|
|
827
|
-
const merged = [
|
|
828
|
-
...listSessionsWithInfo(params.cwd), // project-local first
|
|
829
|
-
...listSessionsWithInfo(), // global fallback
|
|
830
|
-
].filter(s => {
|
|
831
|
-
if (seen.has(s.name))
|
|
832
|
-
return false;
|
|
833
|
-
seen.add(s.name);
|
|
834
|
-
return true;
|
|
835
|
-
});
|
|
836
|
-
// Sort newest first
|
|
837
|
-
merged.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
838
|
-
const acpSessions = merged.map(s => ({
|
|
839
|
-
sessionId: s.name,
|
|
840
|
-
cwd: params.cwd ?? '',
|
|
841
|
-
title: s.title,
|
|
842
|
-
updatedAt: s.createdAt,
|
|
843
|
-
}));
|
|
844
|
-
const result = { sessions: acpSessions };
|
|
845
|
-
transport.respond(msg.id, result);
|
|
753
|
+
handleSessionListExternal(msg, handlerDeps);
|
|
846
754
|
}
|
|
847
755
|
// ── session/delete ───────────────────────────────────────────────────────────
|
|
848
756
|
function handleSessionDelete(msg) {
|
|
849
|
-
|
|
850
|
-
// Remove from in-memory sessions map if present
|
|
851
|
-
sessions.delete(sessionId);
|
|
852
|
-
// Tear down any MCP server processes attached to this session — leaks
|
|
853
|
-
// children otherwise. Fire-and-forget; client doesn't wait on stop().
|
|
854
|
-
disposeMcpSession(sessionId).catch(() => { });
|
|
855
|
-
// Try project dir first, then global
|
|
856
|
-
const deleted = deleteSessionFile(sessionId, cwd || undefined);
|
|
857
|
-
if (!deleted && cwd)
|
|
858
|
-
deleteSessionFile(sessionId);
|
|
859
|
-
transport.respond(msg.id, {});
|
|
757
|
+
handleSessionDeleteExternal(msg, handlerDeps);
|
|
860
758
|
}
|
|
861
759
|
// ── session/list_providers ──────────────────────────────────────────────────
|
|
862
760
|
// Canonical provider list for ACP clients (Codeep VS Code extension etc.).
|
|
@@ -864,24 +762,7 @@ export function startAcpServer() {
|
|
|
864
762
|
// source instead of carrying its own hardcoded copy. Keep this stable —
|
|
865
763
|
// adding new fields is fine, removing/renaming would break existing clients.
|
|
866
764
|
function handleListProviders(msg) {
|
|
867
|
-
|
|
868
|
-
id,
|
|
869
|
-
name: p.name,
|
|
870
|
-
description: p.description,
|
|
871
|
-
groupLabel: p.groupLabel ?? p.name,
|
|
872
|
-
hint: p.hint ?? p.description,
|
|
873
|
-
requiresKey: !p.noApiKey,
|
|
874
|
-
subscribeUrl: p.subscribeUrl,
|
|
875
|
-
// Model metadata so ACP clients (e.g. the VS Code model picker) can
|
|
876
|
-
// offer a provider → model selector without hardcoding a catalog.
|
|
877
|
-
// `dynamicModels` flags providers whose model list is open-ended
|
|
878
|
-
// (OpenRouter, Ollama, custom endpoints) — clients should let the
|
|
879
|
-
// user type a model id rather than only pick from `models`.
|
|
880
|
-
models: p.models.map((m) => ({ id: m.id, name: m.name })),
|
|
881
|
-
defaultModel: p.defaultModel,
|
|
882
|
-
dynamicModels: p.dynamicModels ?? false,
|
|
883
|
-
}));
|
|
884
|
-
transport.respond(msg.id, { providers });
|
|
765
|
+
handleListProvidersExternal(msg, handlerDeps);
|
|
885
766
|
}
|
|
886
767
|
// ── session/prompt ──────────────────────────────────────────────────────────
|
|
887
768
|
async function handleSessionPrompt(msg) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { JsonRpcRequest } from './protocol.js';
|
|
2
|
+
import type { AcpSession } from './commands.js';
|
|
3
|
+
/** Session record held by the running server. Lives in server.ts; re-exposed
|
|
4
|
+
* here so handler signatures can name it without re-declaring the shape. */
|
|
5
|
+
export interface AcpServerSession extends AcpSession {
|
|
6
|
+
abortController: AbortController | null;
|
|
7
|
+
currentModeId: string;
|
|
8
|
+
titleSent: boolean;
|
|
9
|
+
hadHistory: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** Sessions map the server owns. */
|
|
12
|
+
export type SessionMap = Map<string, AcpServerSession>;
|
|
13
|
+
/** The transport surface handlers actually use. Mirrors `StdioTransport`'s
|
|
14
|
+
* public methods — stub this in tests. */
|
|
15
|
+
export interface AcpTransport {
|
|
16
|
+
respond(id: string | number, result: unknown): void;
|
|
17
|
+
error(id: string | number, code: number, message: string): void;
|
|
18
|
+
notify(method: string, params: unknown): void;
|
|
19
|
+
}
|
|
20
|
+
/** Everything a handler needs. Constructed once in `startAcpServer()` and
|
|
21
|
+
* passed to every handler call. */
|
|
22
|
+
export interface AcpHandlerDeps {
|
|
23
|
+
transport: AcpTransport;
|
|
24
|
+
sessions: SessionMap;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Switch the agent confirmation mode for a session (auto = no prompts,
|
|
28
|
+
* manual = prompt before destructive tools). Per-session only — the global
|
|
29
|
+
* `agentConfirmation` config is deliberately NOT mutated, see the comment
|
|
30
|
+
* in the body.
|
|
31
|
+
*/
|
|
32
|
+
export declare function handleSetMode(msg: JsonRpcRequest, deps: AcpHandlerDeps): void;
|
|
33
|
+
/**
|
|
34
|
+
* Apply a session-level config change coming from the ACP client (model picker,
|
|
35
|
+
* language dropdown, confirmation toggles, login field). Mutates the global
|
|
36
|
+
* config (these ARE meant to persist across sessions, unlike mode) and pushes
|
|
37
|
+
* the refreshed config-option list back to the client.
|
|
38
|
+
*/
|
|
39
|
+
export declare function handleSetConfigOption(msg: JsonRpcRequest, deps: AcpHandlerDeps): void;
|
|
40
|
+
/**
|
|
41
|
+
* Pure-ish: apply a single config-option update to the global config.
|
|
42
|
+
* Split out from the handler so it can be tested without a transport.
|
|
43
|
+
*
|
|
44
|
+
* - `model` / `provider`: switch the active provider + model.
|
|
45
|
+
* - `customBaseUrl`: point the `custom` OpenAI-compatible provider at a
|
|
46
|
+
* self-hosted endpoint.
|
|
47
|
+
* - `language`: response language code.
|
|
48
|
+
* - `agentConfirm*` / `userProfile` / `autoLearnProfile`: boolean toggles,
|
|
49
|
+
* accept `true`/`false` or the strings `"true"`/`"false"` (Zed sends strings).
|
|
50
|
+
* - `login`: special-case `"providerId:apiKey"` — sets the provider and writes
|
|
51
|
+
* the key to the keychain. The keychain write is async and fire-and-forget;
|
|
52
|
+
* a persistence failure only means the key isn't durable, it's still usable
|
|
53
|
+
* in-process via the cache.
|
|
54
|
+
*/
|
|
55
|
+
export declare function applyConfigOption(configId: string, value: unknown): void;
|
|
56
|
+
/**
|
|
57
|
+
* List saved sessions (project-scoped first, then global fallback), deduped
|
|
58
|
+
* by name and sorted newest-first.
|
|
59
|
+
*/
|
|
60
|
+
export declare function handleSessionList(msg: JsonRpcRequest, deps: AcpHandlerDeps): void;
|
|
61
|
+
/**
|
|
62
|
+
* Remove a session: drop it from the in-memory map, tear down any MCP child
|
|
63
|
+
* processes attached to it, then delete the on-disk session file (project dir
|
|
64
|
+
* first, global fallback).
|
|
65
|
+
*/
|
|
66
|
+
export declare function handleSessionDelete(msg: JsonRpcRequest, deps: AcpHandlerDeps): void;
|
|
67
|
+
/**
|
|
68
|
+
* Pure shape: map the PROVIDERS table into the serialisable form ACP clients
|
|
69
|
+
* expect (id/name/groupLabel/models/dynamicModels/…). No transport side-effect.
|
|
70
|
+
* Exported so the handler is a one-liner and the shape can be unit-tested.
|
|
71
|
+
*/
|
|
72
|
+
export declare function buildProviderList(): {
|
|
73
|
+
id: string;
|
|
74
|
+
name: string;
|
|
75
|
+
description: string;
|
|
76
|
+
groupLabel: string;
|
|
77
|
+
hint: string;
|
|
78
|
+
requiresKey: boolean;
|
|
79
|
+
subscribeUrl: string | undefined;
|
|
80
|
+
models: {
|
|
81
|
+
id: string;
|
|
82
|
+
name: string;
|
|
83
|
+
}[];
|
|
84
|
+
defaultModel: string;
|
|
85
|
+
dynamicModels: boolean;
|
|
86
|
+
}[];
|
|
87
|
+
/** session/list_providers handler — delegate to the pure builder + respond. */
|
|
88
|
+
export declare function handleListProviders(msg: JsonRpcRequest, deps: AcpHandlerDeps): void;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// src/acp/serverHandlers.ts
|
|
2
|
+
//
|
|
3
|
+
// Session-lifecycle handlers for the ACP server, extracted from the
|
|
4
|
+
// `startAcpServer()` closure in `server.ts` into a factory so they can be
|
|
5
|
+
// unit-tested with a stub transport + sessions map.
|
|
6
|
+
//
|
|
7
|
+
// Each handler is a pure function of (msg, deps) → side-effect on deps.
|
|
8
|
+
// `deps` carries the transport (for respond/error/notify), the sessions map,
|
|
9
|
+
// and a handful of module-scope helpers (buildConfigOptions, AGENT_MODES).
|
|
10
|
+
// No handler reads process-level singletons directly — everything comes in
|
|
11
|
+
// through deps, which is what makes them testable.
|
|
12
|
+
//
|
|
13
|
+
// Not all handlers from server.ts live here yet. The big async ones
|
|
14
|
+
// (`handleSessionPrompt`) reach into MCP spawning, vision APIs, and the agent
|
|
15
|
+
// loop — those stay inside the closure until their dependencies are also
|
|
16
|
+
// lifted. The handlers in this file are the synchronous ones whose contract
|
|
17
|
+
// is "look up the session, mutate config or session state, acknowledge".
|
|
18
|
+
import { AGENT_MODES, buildConfigOptions } from './server.js';
|
|
19
|
+
import { config, setProvider, setApiKey, listSessionsWithInfo, deleteSession as deleteSessionFile, } from '../config/index.js';
|
|
20
|
+
import { disposeSession as disposeMcpSession } from '../utils/mcpRegistry.js';
|
|
21
|
+
// ─── session/set_mode ─────────────────────────────────────────────────────────
|
|
22
|
+
/**
|
|
23
|
+
* Switch the agent confirmation mode for a session (auto = no prompts,
|
|
24
|
+
* manual = prompt before destructive tools). Per-session only — the global
|
|
25
|
+
* `agentConfirmation` config is deliberately NOT mutated, see the comment
|
|
26
|
+
* in the body.
|
|
27
|
+
*/
|
|
28
|
+
export function handleSetMode(msg, deps) {
|
|
29
|
+
const { transport, sessions } = deps;
|
|
30
|
+
const { sessionId, modeId } = msg.params;
|
|
31
|
+
const session = sessions.get(sessionId);
|
|
32
|
+
if (!session) {
|
|
33
|
+
transport.error(msg.id, -32602, `Unknown sessionId: ${sessionId}`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const validMode = AGENT_MODES.availableModes.find(m => m.id === modeId);
|
|
37
|
+
if (!validMode) {
|
|
38
|
+
transport.error(msg.id, -32602, `Unknown modeId: ${modeId}`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
session.currentModeId = modeId;
|
|
42
|
+
// Do NOT persist the global `agentConfirmation` config here. The ACP
|
|
43
|
+
// permission gate is driven per-session by `session.currentModeId` (see the
|
|
44
|
+
// onRequestPermission wiring in handleSessionPrompt). Writing it globally
|
|
45
|
+
// would leak this session's mode into other processes — e.g. switching ACP
|
|
46
|
+
// to 'auto' would silently disarm the TUI's confirmation gate. Mode stays
|
|
47
|
+
// on the in-memory session object only.
|
|
48
|
+
transport.respond(msg.id, {});
|
|
49
|
+
// Notify Zed of the mode change
|
|
50
|
+
transport.notify('session/update', {
|
|
51
|
+
sessionId,
|
|
52
|
+
update: {
|
|
53
|
+
sessionUpdate: 'current_mode_update',
|
|
54
|
+
currentModeId: modeId,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// ─── session/set_config_option ────────────────────────────────────────────────
|
|
59
|
+
/**
|
|
60
|
+
* Apply a session-level config change coming from the ACP client (model picker,
|
|
61
|
+
* language dropdown, confirmation toggles, login field). Mutates the global
|
|
62
|
+
* config (these ARE meant to persist across sessions, unlike mode) and pushes
|
|
63
|
+
* the refreshed config-option list back to the client.
|
|
64
|
+
*/
|
|
65
|
+
export function handleSetConfigOption(msg, deps) {
|
|
66
|
+
const { transport, sessions } = deps;
|
|
67
|
+
const { sessionId, configId, value } = msg.params;
|
|
68
|
+
const session = sessions.get(sessionId);
|
|
69
|
+
if (!session) {
|
|
70
|
+
transport.error(msg.id, -32602, `Unknown sessionId: ${sessionId}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
applyConfigOption(configId, value);
|
|
74
|
+
transport.respond(msg.id, {});
|
|
75
|
+
// Confirm the new value back to Zed so its UI state stays in sync
|
|
76
|
+
transport.notify('session/update', {
|
|
77
|
+
sessionId,
|
|
78
|
+
update: {
|
|
79
|
+
sessionUpdate: 'config_option_update',
|
|
80
|
+
configOptions: buildConfigOptions(),
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Pure-ish: apply a single config-option update to the global config.
|
|
86
|
+
* Split out from the handler so it can be tested without a transport.
|
|
87
|
+
*
|
|
88
|
+
* - `model` / `provider`: switch the active provider + model.
|
|
89
|
+
* - `customBaseUrl`: point the `custom` OpenAI-compatible provider at a
|
|
90
|
+
* self-hosted endpoint.
|
|
91
|
+
* - `language`: response language code.
|
|
92
|
+
* - `agentConfirm*` / `userProfile` / `autoLearnProfile`: boolean toggles,
|
|
93
|
+
* accept `true`/`false` or the strings `"true"`/`"false"` (Zed sends strings).
|
|
94
|
+
* - `login`: special-case `"providerId:apiKey"` — sets the provider and writes
|
|
95
|
+
* the key to the keychain. The keychain write is async and fire-and-forget;
|
|
96
|
+
* a persistence failure only means the key isn't durable, it's still usable
|
|
97
|
+
* in-process via the cache.
|
|
98
|
+
*/
|
|
99
|
+
export function applyConfigOption(configId, value) {
|
|
100
|
+
if (configId === 'model' && typeof value === 'string') {
|
|
101
|
+
// value is "providerId/modelId" — split and switch both
|
|
102
|
+
const slashIdx = value.indexOf('/');
|
|
103
|
+
if (slashIdx !== -1) {
|
|
104
|
+
const providerId = value.slice(0, slashIdx);
|
|
105
|
+
const modelId = value.slice(slashIdx + 1);
|
|
106
|
+
setProvider(providerId); // sets provider + defaultModel + protocol
|
|
107
|
+
config.set('model', modelId);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
config.set('model', value);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else if (configId === 'provider' && typeof value === 'string') {
|
|
114
|
+
// Switch provider without specifying a model — picks the provider's
|
|
115
|
+
// default model + protocol. Used by editor clients that pin a provider
|
|
116
|
+
// in their settings (e.g. the VS Code `codeep.provider` setting).
|
|
117
|
+
setProvider(value);
|
|
118
|
+
}
|
|
119
|
+
else if (configId === 'customBaseUrl' && typeof value === 'string') {
|
|
120
|
+
// Base URL for the `custom` (OpenAI-compatible) provider — lets editor
|
|
121
|
+
// clients point Codeep at a self-hosted endpoint (vLLM/LiteLLM/LM Studio)
|
|
122
|
+
// without hand-editing ~/.codeep/config.json.
|
|
123
|
+
config.set('customBaseUrl', value);
|
|
124
|
+
}
|
|
125
|
+
else if (configId === 'language' && typeof value === 'string') {
|
|
126
|
+
config.set('language', value);
|
|
127
|
+
}
|
|
128
|
+
else if (configId === 'agentConfirmDeleteFile' ||
|
|
129
|
+
configId === 'agentConfirmExecuteCommand' ||
|
|
130
|
+
configId === 'agentConfirmWriteFile' ||
|
|
131
|
+
configId === 'userProfile' ||
|
|
132
|
+
configId === 'autoLearnProfile') {
|
|
133
|
+
// Accept boolean or "true"/"false" string from Zed/VSCode
|
|
134
|
+
const bool = value === true || value === 'true';
|
|
135
|
+
config.set(configId, bool);
|
|
136
|
+
}
|
|
137
|
+
else if (configId === 'login' && typeof value === 'string') {
|
|
138
|
+
// value is "providerId:apiKey"
|
|
139
|
+
const colonIdx = value.indexOf(':');
|
|
140
|
+
if (colonIdx !== -1) {
|
|
141
|
+
const providerId = value.slice(0, colonIdx);
|
|
142
|
+
const apiKey = value.slice(colonIdx + 1);
|
|
143
|
+
if (providerId && apiKey) {
|
|
144
|
+
setProvider(providerId);
|
|
145
|
+
// Cache is updated synchronously inside setApiKey; the keychain write
|
|
146
|
+
// resolves while the long-lived server runs. Swallow a persistence
|
|
147
|
+
// failure here (secondary path) so it can't become an unhandled
|
|
148
|
+
// rejection — the key still works for this session via the cache.
|
|
149
|
+
setApiKey(apiKey, providerId).catch(() => { });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// ─── session/list ─────────────────────────────────────────────────────────────
|
|
155
|
+
/**
|
|
156
|
+
* List saved sessions (project-scoped first, then global fallback), deduped
|
|
157
|
+
* by name and sorted newest-first.
|
|
158
|
+
*/
|
|
159
|
+
export function handleSessionList(msg, deps) {
|
|
160
|
+
const { transport } = deps;
|
|
161
|
+
const params = (msg.params ?? {});
|
|
162
|
+
// Collect local (project-scoped) sessions and global sessions, deduplicated by name
|
|
163
|
+
const seen = new Set();
|
|
164
|
+
const merged = [
|
|
165
|
+
...listSessionsWithInfo(params.cwd), // project-local first
|
|
166
|
+
...listSessionsWithInfo(), // global fallback
|
|
167
|
+
].filter(s => {
|
|
168
|
+
if (seen.has(s.name))
|
|
169
|
+
return false;
|
|
170
|
+
seen.add(s.name);
|
|
171
|
+
return true;
|
|
172
|
+
});
|
|
173
|
+
// Sort newest first
|
|
174
|
+
merged.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
175
|
+
const acpSessions = merged.map(s => ({
|
|
176
|
+
sessionId: s.name,
|
|
177
|
+
cwd: params.cwd ?? '',
|
|
178
|
+
title: s.title,
|
|
179
|
+
updatedAt: s.createdAt,
|
|
180
|
+
}));
|
|
181
|
+
const result = { sessions: acpSessions };
|
|
182
|
+
transport.respond(msg.id, result);
|
|
183
|
+
}
|
|
184
|
+
// ─── session/delete ───────────────────────────────────────────────────────────
|
|
185
|
+
/**
|
|
186
|
+
* Remove a session: drop it from the in-memory map, tear down any MCP child
|
|
187
|
+
* processes attached to it, then delete the on-disk session file (project dir
|
|
188
|
+
* first, global fallback).
|
|
189
|
+
*/
|
|
190
|
+
export function handleSessionDelete(msg, deps) {
|
|
191
|
+
const { transport, sessions } = deps;
|
|
192
|
+
const { sessionId, cwd } = (msg.params ?? {});
|
|
193
|
+
// Remove from in-memory sessions map if present
|
|
194
|
+
sessions.delete(sessionId);
|
|
195
|
+
// Tear down any MCP server processes attached to this session — leaks
|
|
196
|
+
// children otherwise. Fire-and-forget; client doesn't wait on stop().
|
|
197
|
+
disposeMcpSession(sessionId).catch(() => { });
|
|
198
|
+
// Try project dir first, then global
|
|
199
|
+
const deleted = deleteSessionFile(sessionId, cwd || undefined);
|
|
200
|
+
if (!deleted && cwd)
|
|
201
|
+
deleteSessionFile(sessionId);
|
|
202
|
+
transport.respond(msg.id, {});
|
|
203
|
+
}
|
|
204
|
+
// ─── session/list_providers ───────────────────────────────────────────────────
|
|
205
|
+
// Canonical provider list for ACP clients (Codeep VS Code extension etc.).
|
|
206
|
+
// Lets the client populate its API-key dropdowns and group labels from one
|
|
207
|
+
// source instead of carrying its own hardcoded copy. Keep this stable —
|
|
208
|
+
// adding new fields is fine, removing/renaming would break existing clients.
|
|
209
|
+
import { PROVIDERS } from '../config/providers.js';
|
|
210
|
+
/**
|
|
211
|
+
* Pure shape: map the PROVIDERS table into the serialisable form ACP clients
|
|
212
|
+
* expect (id/name/groupLabel/models/dynamicModels/…). No transport side-effect.
|
|
213
|
+
* Exported so the handler is a one-liner and the shape can be unit-tested.
|
|
214
|
+
*/
|
|
215
|
+
export function buildProviderList() {
|
|
216
|
+
return Object.entries(PROVIDERS).map(([id, p]) => ({
|
|
217
|
+
id,
|
|
218
|
+
name: p.name,
|
|
219
|
+
description: p.description,
|
|
220
|
+
groupLabel: p.groupLabel ?? p.name,
|
|
221
|
+
hint: p.hint ?? p.description,
|
|
222
|
+
requiresKey: !p.noApiKey,
|
|
223
|
+
subscribeUrl: p.subscribeUrl,
|
|
224
|
+
// Model metadata so ACP clients (e.g. the VS Code model picker) can
|
|
225
|
+
// offer a provider → model selector without hardcoding a catalog.
|
|
226
|
+
// `dynamicModels` flags providers whose model list is open-ended
|
|
227
|
+
// (OpenRouter, Ollama, custom endpoints) — clients should let the
|
|
228
|
+
// user type a model id rather than only pick from `models`.
|
|
229
|
+
models: p.models.map((m) => ({ id: m.id, name: m.name })),
|
|
230
|
+
defaultModel: p.defaultModel,
|
|
231
|
+
dynamicModels: p.dynamicModels ?? false,
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
/** session/list_providers handler — delegate to the pure builder + respond. */
|
|
235
|
+
export function handleListProviders(msg, deps) {
|
|
236
|
+
deps.transport.respond(msg.id, { providers: buildProviderList() });
|
|
237
|
+
}
|