@code-yeongyu/senpi 2026.10.8 → 2026.10.10-2
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/CHANGELOG.md +58 -0
- package/dist/bundle/chunks/{app-server-command-MJOYE5ZS.js → app-server-command-WZG7WYMD.js} +1 -1
- package/dist/bundle/chunks/{chunk-MJGHZT4N.js → chunk-3A3C5VVO.js} +1 -1
- package/dist/bundle/chunks/{chunk-4GUQVY4Z.js → chunk-3YXB7Z7O.js} +1 -1
- package/dist/bundle/chunks/{chunk-CB77NQMN.js → chunk-7GVD54RM.js} +1 -1
- package/dist/bundle/chunks/{chunk-6EH7QXOG.js → chunk-CTERKXVZ.js} +2 -2
- package/dist/bundle/chunks/{chunk-CM5QTRC2.js → chunk-H67EVDWG.js} +1 -1
- package/dist/bundle/chunks/chunk-IZOTKUWG.js +2 -0
- package/dist/bundle/chunks/{chunk-K4AZKFRB.js → chunk-J5MUMVWB.js} +2 -2
- package/dist/bundle/chunks/{chunk-V2VQ4BHG.js → chunk-M2IHGL3M.js} +1 -1
- package/dist/bundle/chunks/{chunk-LUWWM7GA.js → chunk-RADHRQ4K.js} +3 -3
- package/dist/bundle/chunks/chunk-RRU5LXMS.js +3 -0
- package/dist/bundle/chunks/{chunk-BTYFICUC.js → chunk-SB22X6ZR.js} +1 -1
- package/dist/bundle/chunks/chunk-ULYOMGXX.js +14 -0
- package/dist/bundle/chunks/{chunk-7Q3EY6HH.js → chunk-UMJL74VN.js} +1 -1
- package/dist/bundle/chunks/{chunk-5MG76BCM.js → chunk-W6B7SEAK.js} +180 -178
- package/dist/bundle/chunks/{cli-main-GWVLFOIP.js → cli-main-T4T5UNRJ.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-JBQLYO7W.js → help-fast-path-44VUGB7H.js} +1 -1
- package/dist/bundle/chunks/{host-command-HTYYQBSG.js → host-command-X64D7PT3.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-XZF6IKKO.js → host-lifecycle-GC3GIR2N.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-YLM2U7IX.js → interactive-mode-ULFSI46Z.js} +1 -1
- package/dist/bundle/chunks/{list-tips-JQDZPB6C.js → list-tips-IIRFXBTO.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-KMCBBPTV.js → multi-session-host-VM7WJBC5.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-3UIJ4RXY.js → package-manager-cli-OAFUQY7E.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-PUYBZG27.js → rpc-mode-XVRJJG3K.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-CVAPK2WQ.js → schedule-command-EQ4PQCRP.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-THEF3K7M.js → session-control-endpoint-FEGOYWHN.js} +2 -2
- package/dist/bundle/chunks/{session-picker-N463VCDI.js → session-picker-R5N2EKUS.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +95 -93
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session-runtime.d.ts +9 -0
- package/dist/core/agent-session-runtime.js +22 -0
- package/dist/core/agent-session.d.ts +12 -1
- package/dist/core/agent-session.js +38 -4
- package/dist/core/dynamic-prompt/handoff.js +3 -3
- package/dist/core/dynamic-prompt/intent-gate.js +1 -1
- package/dist/core/dynamic-prompt/style.js +1 -1
- package/dist/core/extensions/builtin/btw/panel.d.ts +3 -1
- package/dist/core/extensions/builtin/btw/panel.js +14 -9
- package/dist/core/extensions/builtin/compaction/context-pipeline.d.ts +5 -0
- package/dist/core/extensions/builtin/compaction/context-pipeline.js +1 -0
- package/dist/core/extensions/builtin/compaction/index.js +2 -1
- package/dist/core/extensions/builtin/compaction/lane-policy.d.ts +7 -0
- package/dist/core/extensions/builtin/compaction/lane-policy.js +28 -15
- package/dist/core/extensions/builtin/compaction/log.d.ts +2 -0
- package/dist/core/extensions/builtin/compaction/log.js +2 -1
- package/dist/core/extensions/builtin/goal/cache-warm-renderer.js +14 -8
- package/dist/core/extensions/builtin/goal/cache-warm.d.ts +4 -2
- package/dist/core/extensions/builtin/goal/monitor-continuation.js +6 -0
- package/dist/core/extensions/builtin/mcp/commands.js +21 -26
- package/dist/core/extensions/builtin/mcp/config-edit.d.ts +2 -0
- package/dist/core/extensions/builtin/mcp/config-edit.js +5 -2
- package/dist/core/extensions/builtin/mcp/manager-view.d.ts +34 -0
- package/dist/core/extensions/builtin/mcp/manager-view.js +136 -0
- package/dist/core/extensions/builtin/mcp/manager.d.ts +8 -0
- package/dist/core/extensions/builtin/mcp/manager.js +193 -0
- package/dist/core/extensions/builtin/mcp/service.d.ts +3 -1
- package/dist/core/extensions/builtin/mcp/service.js +4 -0
- package/dist/core/extensions/builtin/permission-system/index.js +4 -0
- package/dist/core/extensions/builtin/permission-system/prompt.js +10 -2
- package/dist/core/extensions/builtin/permission-system/types.d.ts +3 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.js +2 -2
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js +2 -2
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-5-5.js +2 -2
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-5.js +2 -2
- package/dist/core/extensions/builtin/prompt-preset/claude-sonnet-5-5.js +2 -2
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.js +3 -3
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js +3 -3
- package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.d.ts +2 -2
- package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.js +4 -4
- package/dist/core/extensions/builtin/prompt-preset/gpt-surface.js +2 -2
- package/dist/core/extensions/builtin/prompt-preset/grok-4.5.js +2 -2
- package/dist/core/extensions/builtin/prompt-preset/grok-4.6.js +1 -1
- package/dist/core/extensions/builtin/prompt-preset/grok-4.7.js +1 -1
- package/dist/core/extensions/builtin/prompt-preset/kimi-k3.js +2 -2
- package/dist/core/extensions/builtin/rules/rules/finder.js +8 -3
- package/dist/core/extensions/types.d.ts +4 -0
- package/dist/core/retry-fallback/expansion.js +9 -3
- package/dist/core/sdk.js +7 -3
- package/dist/core/thinking-levels.d.ts +23 -1
- package/dist/core/thinking-levels.js +37 -0
- package/dist/core/trust-manager.js +50 -0
- package/dist/main.js +12 -13
- package/dist/modes/interactive/interactive-mode.js +8 -0
- package/dist/modes/interactive/session-control-endpoint.js +33 -8
- package/dist/modes/interactive/session-control-registry.js +3 -1
- package/dist/modes/interactive/session-control-wake.d.ts +11 -5
- package/dist/modes/interactive/session-control-wake.js +38 -16
- package/dist/modes/rpc/connection-handler.d.ts +5 -0
- package/dist/modes/rpc/connection-handler.js +41 -5
- package/dist/modes/rpc/custom-capability.d.ts +6 -0
- package/dist/modes/rpc/custom-capability.js +6 -0
- package/dist/modes/rpc/host-daemon-paths.d.ts +5 -4
- package/dist/modes/rpc/host-daemon-paths.js +17 -9
- package/dist/modes/rpc/host-daemon-registration.d.ts +6 -1
- package/dist/modes/rpc/host-daemon-registration.js +6 -2
- package/dist/modes/rpc/host-session-control.js +6 -4
- package/dist/modes/rpc/rpc-client.d.ts +5 -0
- package/dist/modes/rpc/rpc-client.js +7 -0
- package/dist/modes/rpc/rpc-mode.js +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +14 -0
- package/docs/json.md +3 -0
- package/docs/mcp.md +16 -1
- package/docs/rpc.md +49 -1
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-6SMU7ANE.js +0 -3
- package/dist/bundle/chunks/chunk-VJJ53ORU.js +0 -2
- package/dist/bundle/chunks/chunk-X6UDRO7J.js +0 -14
|
@@ -66,6 +66,23 @@ export function createCompactionLanePolicy(options = {}) {
|
|
|
66
66
|
const load = options.loadProviderSettings ?? loadAnthropicSubscriptionProviderSettingsFromDisk;
|
|
67
67
|
let cachedCwd;
|
|
68
68
|
let cachedResumeMode;
|
|
69
|
+
// Per-cwd cache is the intended contract (pinned by lane-policy.test.ts):
|
|
70
|
+
// resumeMode is read once per cwd. A mid-session switch takes effect on
|
|
71
|
+
// the next cwd or session. Returns false when the settings cannot be read,
|
|
72
|
+
// so callers fail closed to senpi's full behavior.
|
|
73
|
+
const resolveResumeMode = (cwd) => {
|
|
74
|
+
if (cachedCwd === cwd)
|
|
75
|
+
return true;
|
|
76
|
+
try {
|
|
77
|
+
cachedResumeMode = load(cwd).resumeMode;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
cachedCwd = undefined;
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
cachedCwd = cwd;
|
|
84
|
+
return true;
|
|
85
|
+
};
|
|
69
86
|
// Declared as a local so `ownsCompaction` never depends on `this`: the policy object
|
|
70
87
|
// is routinely destructured at call sites, which would otherwise unbind the receiver.
|
|
71
88
|
const disablesSenpiCompaction = (context) => {
|
|
@@ -76,25 +93,21 @@ export function createCompactionLanePolicy(options = {}) {
|
|
|
76
93
|
// the escape hatch for lanes whose SDK never fires native compaction.
|
|
77
94
|
if (context.getCompactionSettings?.().model)
|
|
78
95
|
return false;
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
cachedResumeMode = load(context.cwd).resumeMode;
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
// A settings read failure must never silently disable senpi compaction:
|
|
88
|
-
// fail closed by keeping senpi's own compaction fully active.
|
|
89
|
-
cachedCwd = undefined;
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
cachedCwd = context.cwd;
|
|
93
|
-
}
|
|
96
|
+
// A settings read failure must never silently disable senpi compaction:
|
|
97
|
+
// fail closed by keeping senpi's own compaction fully active.
|
|
98
|
+
if (!resolveResumeMode(context.cwd))
|
|
99
|
+
return false;
|
|
94
100
|
return isSdkNativeCompactionLane({ model: context.model, resumeMode: cachedResumeMode });
|
|
95
101
|
};
|
|
96
102
|
return {
|
|
97
103
|
disablesSenpiCompaction,
|
|
104
|
+
hasAppendOnlyTranscript(context) {
|
|
105
|
+
if (context.model?.provider !== ANTHROPIC_SUBSCRIPTION_PROVIDER_ID)
|
|
106
|
+
return false;
|
|
107
|
+
if (!resolveResumeMode(context.cwd))
|
|
108
|
+
return false;
|
|
109
|
+
return cachedResumeMode !== "off";
|
|
110
|
+
},
|
|
98
111
|
ownsCompaction(context, reason) {
|
|
99
112
|
// Manual is senpi-owned everywhere: it is the user's explicit recovery path,
|
|
100
113
|
// including on an SDK-native lane whose automatic routes stay SDK-owned.
|
|
@@ -6,6 +6,7 @@ export interface CompactionLoggerData {
|
|
|
6
6
|
variant?: string;
|
|
7
7
|
generation?: number;
|
|
8
8
|
requestId?: string;
|
|
9
|
+
sessionId?: string;
|
|
9
10
|
tokens?: number;
|
|
10
11
|
tokensBefore?: number;
|
|
11
12
|
savedTokens?: number;
|
|
@@ -21,6 +22,7 @@ export interface CompactionLogger {
|
|
|
21
22
|
info(event: CompactionLoggerEvent, data?: CompactionLoggerData): void;
|
|
22
23
|
}
|
|
23
24
|
export interface CompactionLoggerOptions {
|
|
25
|
+
getSessionId?: () => string;
|
|
24
26
|
sink?: (line: string) => void;
|
|
25
27
|
mirrorToStderr?: boolean;
|
|
26
28
|
maxBytes?: number;
|
|
@@ -10,6 +10,7 @@ const ALLOWED_KEYS = new Set([
|
|
|
10
10
|
"variant",
|
|
11
11
|
"generation",
|
|
12
12
|
"requestId",
|
|
13
|
+
"sessionId",
|
|
13
14
|
"tokens",
|
|
14
15
|
"tokensBefore",
|
|
15
16
|
"savedTokens",
|
|
@@ -55,7 +56,7 @@ export function createCompactionLogger(agentDir, options = {}) {
|
|
|
55
56
|
try {
|
|
56
57
|
if (!EVENTS.has(event))
|
|
57
58
|
return;
|
|
58
|
-
const line = formatLine(level, event, data);
|
|
59
|
+
const line = formatLine(level, event, { ...data, sessionId: options.getSessionId?.() ?? data?.sessionId });
|
|
59
60
|
writeLine(filePath, line, maxBytes, options.sink);
|
|
60
61
|
if (options.mirrorToStderr ?? envValue("COMPACTION_DEBUG") === "1") {
|
|
61
62
|
console.error(DEBUG_PREFIX, line);
|
|
@@ -28,7 +28,7 @@ function titleLine(data) {
|
|
|
28
28
|
case "scheduled":
|
|
29
29
|
return `⚡ Cache-warm wait${iterationText} · ${wakeSources}`;
|
|
30
30
|
case "resumed":
|
|
31
|
-
return `⚡ Cache-warm wake${iterationText} · ${
|
|
31
|
+
return `⚡ Cache-warm wake${iterationText} · waited ${formatWakeDuration(data.waitedMs ?? data.delayMs)} · ${wakeSources}`;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
function validIteration(value) {
|
|
@@ -48,7 +48,14 @@ function whyLine(data) {
|
|
|
48
48
|
: `${backstop} - the goal resumes as soon as a wake source delivers; the ${formatCacheTtl(data.cache.ttlSeconds)} prompt-cache TTL may elapse first.`;
|
|
49
49
|
}
|
|
50
50
|
case "resumed":
|
|
51
|
-
|
|
51
|
+
switch (data.wakeCause) {
|
|
52
|
+
case "timer":
|
|
53
|
+
return "The stall backstop fired; queued the goal continuation.";
|
|
54
|
+
case "sources-drained":
|
|
55
|
+
return "Wake sources finished; queued the goal continuation.";
|
|
56
|
+
case undefined:
|
|
57
|
+
return "Queued the goal continuation; the wake trigger was not recorded.";
|
|
58
|
+
}
|
|
52
59
|
}
|
|
53
60
|
}
|
|
54
61
|
function formatExpectedWake(dueAtMs, elapsedMs) {
|
|
@@ -66,18 +73,17 @@ function warmLine(data) {
|
|
|
66
73
|
const cache = data.cache;
|
|
67
74
|
if (cache === undefined || cache.cachedTokens <= 0)
|
|
68
75
|
return undefined;
|
|
69
|
-
const
|
|
76
|
+
const body = `Prior turn: ~${formatWarmTokenCount(cache.cachedTokens)} cache-read/write tokens (cumulative)`;
|
|
70
77
|
if (cache.cacheLifetime === "best-effort") {
|
|
71
|
-
return `${
|
|
78
|
+
return `${body} · provider caching is best-effort; next cache hit unverified`;
|
|
72
79
|
}
|
|
73
80
|
const ttlMayHaveElapsed = cache.ttlSeconds !== undefined && (data.waitedMs ?? data.delayMs) >= cache.ttlSeconds * 1000;
|
|
74
81
|
if (ttlMayHaveElapsed) {
|
|
75
|
-
return `${
|
|
82
|
+
return `${body} · prompt-cache TTL may have elapsed; next cache hit unverified`;
|
|
76
83
|
}
|
|
77
|
-
const body = data.phase === "scheduled" ? `${tokens} kept warm` : `${tokens} stayed warm in the prompt cache`;
|
|
78
84
|
const saved = cache.estimatedSavedUsd !== undefined && cache.estimatedSavedUsd > 0
|
|
79
|
-
? ` · est. ${formatSavedUsd(cache.estimatedSavedUsd)}
|
|
85
|
+
? ` · est. ${formatSavedUsd(cache.estimatedSavedUsd)} discount if reused`
|
|
80
86
|
: "";
|
|
81
|
-
return `${body}${saved}`;
|
|
87
|
+
return `${body}${saved} · next cache hit unverified`;
|
|
82
88
|
}
|
|
83
89
|
//# sourceMappingURL=cache-warm-renderer.js.map
|
|
@@ -49,9 +49,9 @@ export interface GoalCacheWarmMetrics {
|
|
|
49
49
|
readonly ttlSeconds?: number;
|
|
50
50
|
/** Set when the provider caches automatically with no expiry contract; no TTL or savings are claimed. */
|
|
51
51
|
readonly cacheLifetime?: "best-effort";
|
|
52
|
-
/**
|
|
52
|
+
/** Cumulative cache-read/write tokens reported across the prior agent run's requests, not unique context size. */
|
|
53
53
|
readonly cachedTokens: number;
|
|
54
|
-
/**
|
|
54
|
+
/** Hypothetical read discount at current model prices if the same token volume is reused from cache. */
|
|
55
55
|
readonly estimatedSavedUsd?: number;
|
|
56
56
|
}
|
|
57
57
|
export type GoalCacheWarmupPhase = "scheduled" | "resumed";
|
|
@@ -71,6 +71,8 @@ export interface GoalCacheWarmupEntryData {
|
|
|
71
71
|
readonly dueAtMs?: number;
|
|
72
72
|
/** Actual wait in milliseconds; present on the `resumed` phase only. */
|
|
73
73
|
readonly waitedMs?: number;
|
|
74
|
+
/** Actual resumption trigger; absent on legacy entries whose cause was not recorded. */
|
|
75
|
+
readonly wakeCause?: "timer" | "sources-drained";
|
|
74
76
|
/** Backward-compatible field containing the total active wake-source count. */
|
|
75
77
|
readonly activeMonitorCount: number;
|
|
76
78
|
/** Full source-keyed snapshot; absent on entries written before wake sources were generalized. */
|
|
@@ -394,6 +394,8 @@ export class MonitorAwareGoalContinuation {
|
|
|
394
394
|
const delayMs = this.#scheduledDelayMs ??
|
|
395
395
|
(kind === "monitor" ? GOAL_MONITOR_CONTINUATION_FALLBACK_DELAY_MS : GOAL_USER_GRACE_DELAY_MS);
|
|
396
396
|
const waitedMs = this.#scheduledAtMs === undefined ? delayMs : Math.max(0, Date.now() - this.#scheduledAtMs);
|
|
397
|
+
const dueAtMs = this.#scheduledAtMs === undefined ? undefined : this.#scheduledAtMs + delayMs;
|
|
398
|
+
const wakeCause = drainFire ? "sources-drained" : "timer";
|
|
397
399
|
const cache = this.#scheduledCache;
|
|
398
400
|
const iteration = this.#scheduledCacheWarmIteration;
|
|
399
401
|
this.#scheduledAtMs = undefined;
|
|
@@ -413,7 +415,9 @@ export class MonitorAwareGoalContinuation {
|
|
|
413
415
|
this.#pi.events?.emit(GOAL_CONTINUATION_RESUMED_EVENT, {
|
|
414
416
|
goalId: goal.id,
|
|
415
417
|
delayMs,
|
|
418
|
+
dueAtMs,
|
|
416
419
|
waitedMs,
|
|
420
|
+
wakeCause,
|
|
417
421
|
iteration,
|
|
418
422
|
activeMonitorCount: this.#activeWakeSourceCount(),
|
|
419
423
|
wakeSources,
|
|
@@ -423,7 +427,9 @@ export class MonitorAwareGoalContinuation {
|
|
|
423
427
|
phase: "resumed",
|
|
424
428
|
goalId: goal.id,
|
|
425
429
|
delayMs,
|
|
430
|
+
dueAtMs,
|
|
426
431
|
waitedMs,
|
|
432
|
+
wakeCause,
|
|
427
433
|
iteration,
|
|
428
434
|
activeMonitorCount: this.#activeWakeSourceCount(),
|
|
429
435
|
wakeSources,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { noticeEntryRenderer } from "../../notice/index.js";
|
|
2
2
|
import { handleMcpAuthCommand } from "./auth/commands-auth-dispatch.js";
|
|
3
3
|
import { addGlobalMcpServer, setGlobalMcpServerEnabled } from "./config-edit.js";
|
|
4
|
+
import { showMcpManager } from "./manager.js";
|
|
4
5
|
import { getMcpService } from "./service.js";
|
|
5
6
|
import { MCP_STARTUP_RACE_MS } from "./startup-race.js";
|
|
6
7
|
import { buildMcpStatusRows, formatMcpStatus } from "./status.js";
|
|
@@ -29,7 +30,7 @@ export function registerMcpCommands(pi, service = getMcpService(), pendingAttach
|
|
|
29
30
|
// in flight. Every subcommand reports or mutates attached state, so wait for the single
|
|
30
31
|
// in-flight attach rather than rendering a half-connected snapshot.
|
|
31
32
|
await pendingAttach();
|
|
32
|
-
await handleMcpCommand(rawArgs, ctx, pi, service);
|
|
33
|
+
await handleMcpCommand(splitCommandArgs(rawArgs), ctx, pi, service);
|
|
33
34
|
}
|
|
34
35
|
catch (error) {
|
|
35
36
|
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
@@ -37,11 +38,15 @@ export function registerMcpCommands(pi, service = getMcpService(), pendingAttach
|
|
|
37
38
|
},
|
|
38
39
|
});
|
|
39
40
|
}
|
|
40
|
-
async function handleMcpCommand(
|
|
41
|
-
const args = splitCommandArgs(rawArgs);
|
|
41
|
+
async function handleMcpCommand(args, ctx, pi, service, notify) {
|
|
42
42
|
const subcommand = args[0] ?? "";
|
|
43
43
|
if (subcommand === "") {
|
|
44
|
-
|
|
44
|
+
if (!ctx.hasUI || ctx.mode !== "tui") {
|
|
45
|
+
ctx.ui.notify(await renderStatus("MCP servers", service));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
await showMcpManager(ctx, pi, service, (command, name, commandCtx, commandNotify) => handleMcpCommand([command, name], commandCtx, pi, service, commandNotify));
|
|
49
|
+
}
|
|
45
50
|
return;
|
|
46
51
|
}
|
|
47
52
|
if (AUTH_SUBCOMMANDS.has(subcommand)) {
|
|
@@ -61,7 +66,7 @@ async function handleMcpCommand(rawArgs, ctx, pi, service) {
|
|
|
61
66
|
return;
|
|
62
67
|
}
|
|
63
68
|
if (subcommand === "test") {
|
|
64
|
-
await testServer(args[1] ?? "", ctx, service);
|
|
69
|
+
await testServer(args[1] ?? "", ctx, service, notify);
|
|
65
70
|
return;
|
|
66
71
|
}
|
|
67
72
|
if (subcommand === "logs") {
|
|
@@ -69,21 +74,11 @@ async function handleMcpCommand(rawArgs, ctx, pi, service) {
|
|
|
69
74
|
return;
|
|
70
75
|
}
|
|
71
76
|
if (subcommand === "reconnect") {
|
|
72
|
-
await reconnectServer(args[1] ?? "", ctx, pi, service);
|
|
77
|
+
await reconnectServer(args[1] ?? "", ctx, pi, service, notify);
|
|
73
78
|
return;
|
|
74
79
|
}
|
|
75
80
|
ctx.ui.notify(`Unknown /mcp subcommand: ${subcommand}`, "error");
|
|
76
81
|
}
|
|
77
|
-
async function showPanel(ctx, service) {
|
|
78
|
-
const text = await renderStatus("MCP servers", service);
|
|
79
|
-
if (!ctx.hasUI) {
|
|
80
|
-
ctx.ui.notify(text);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
const choice = await ctx.ui.select(text, ["status", "logs <server>", "test <server>"]);
|
|
84
|
-
if (choice === undefined)
|
|
85
|
-
ctx.ui.notify(text);
|
|
86
|
-
}
|
|
87
82
|
async function notifyStatus(ctx, service) {
|
|
88
83
|
ctx.ui.notify(await renderStatus("MCP status", service));
|
|
89
84
|
}
|
|
@@ -122,8 +117,8 @@ async function setServerEnabled(ctx, pi, service, name, enabled) {
|
|
|
122
117
|
await service.attachSession({ type: "session_start", reason: "reload" }, ctx, pi);
|
|
123
118
|
ctx.ui.notify(`${enabled ? "Enabled" : "Disabled"} MCP server ${name}`);
|
|
124
119
|
}
|
|
125
|
-
async function testServer(name, ctx, service) {
|
|
126
|
-
if (!ensureKnown(name, ctx, service))
|
|
120
|
+
async function testServer(name, ctx, service, notify = (text, type) => ctx.ui.notify(text, type)) {
|
|
121
|
+
if (!ensureKnown(name, ctx, service, notify))
|
|
127
122
|
return;
|
|
128
123
|
const connection = service.getConnection(name);
|
|
129
124
|
if (connection === undefined)
|
|
@@ -134,13 +129,13 @@ async function testServer(name, ctx, service) {
|
|
|
134
129
|
const result = await connection.client.listTools({}, { timeout: 2000 });
|
|
135
130
|
const elapsedMs = Date.now() - started;
|
|
136
131
|
service.recordCall(name, elapsedMs, false);
|
|
137
|
-
|
|
132
|
+
notify(`MCP test ${name} ok (${elapsedMs}ms): ${result.tools.length} tools`);
|
|
138
133
|
}
|
|
139
134
|
catch (error) {
|
|
140
135
|
const elapsedMs = Date.now() - started;
|
|
141
136
|
service.recordCall(name, elapsedMs, true);
|
|
142
137
|
const message = error instanceof Error ? error.message : String(error);
|
|
143
|
-
|
|
138
|
+
notify(`MCP test ${name} failed (${elapsedMs}ms): ${message}`, "error");
|
|
144
139
|
}
|
|
145
140
|
}
|
|
146
141
|
function showLogs(name, ctx, service) {
|
|
@@ -149,27 +144,27 @@ function showLogs(name, ctx, service) {
|
|
|
149
144
|
const lines = service.getLogLines(name, 20);
|
|
150
145
|
ctx.ui.notify(lines.length === 0 ? `MCP logs for ${name}: (empty)` : lines.join("\n"));
|
|
151
146
|
}
|
|
152
|
-
async function reconnectServer(name, ctx, pi, service) {
|
|
153
|
-
if (!ensureKnown(name, ctx, service))
|
|
147
|
+
async function reconnectServer(name, ctx, pi, service, notify = (text, type) => ctx.ui.notify(text, type)) {
|
|
148
|
+
if (!ensureKnown(name, ctx, service, notify))
|
|
154
149
|
return;
|
|
155
150
|
try {
|
|
156
151
|
await service.reconnectServer(name);
|
|
157
152
|
await service.attachSession({ type: "session_start", reason: "reload" }, ctx, pi);
|
|
158
|
-
|
|
153
|
+
notify(`MCP reconnect ${name} connected`);
|
|
159
154
|
}
|
|
160
155
|
catch (error) {
|
|
161
156
|
const message = error instanceof Error ? error.message : String(error);
|
|
162
|
-
|
|
157
|
+
notify(`MCP reconnect ${name} failed: ${message}`, "error");
|
|
163
158
|
}
|
|
164
159
|
}
|
|
165
|
-
function ensureKnown(name, ctx, service) {
|
|
160
|
+
function ensureKnown(name, ctx, service, notify = (text, type) => ctx.ui.notify(text, type)) {
|
|
166
161
|
if (name.length > 0 && service.getServerSnapshots().some((snapshot) => snapshot.name === name))
|
|
167
162
|
return true;
|
|
168
163
|
const known = service
|
|
169
164
|
.getServerSnapshots()
|
|
170
165
|
.map((snapshot) => snapshot.name)
|
|
171
166
|
.join(", ");
|
|
172
|
-
|
|
167
|
+
notify(`Unknown MCP server: ${name || "<missing>"}\nKnown MCP servers: ${known || "(none)"}`, "error");
|
|
173
168
|
return false;
|
|
174
169
|
}
|
|
175
170
|
function parseServerConfig(endpoint) {
|
|
@@ -2,4 +2,6 @@ import { type McpServerConfig } from "./config-schema.ts";
|
|
|
2
2
|
export declare function getGlobalMcpConfigPath(agentDir?: string): string;
|
|
3
3
|
export declare function addGlobalMcpServer(name: string, server: McpServerConfig): string;
|
|
4
4
|
export declare function setGlobalMcpServerEnabled(name: string, enabled: boolean): boolean;
|
|
5
|
+
/** Update only a resolved global/project source, retaining placeholders and unrelated fields. */
|
|
6
|
+
export declare function updateMcpServerConfig(path: string, name: string, patch: Readonly<Pick<Partial<McpServerConfig>, "enabled" | "exposure">>): boolean;
|
|
5
7
|
//# sourceMappingURL=config-edit.d.ts.map
|
|
@@ -14,14 +14,17 @@ export function addGlobalMcpServer(name, server) {
|
|
|
14
14
|
return path;
|
|
15
15
|
}
|
|
16
16
|
export function setGlobalMcpServerEnabled(name, enabled) {
|
|
17
|
-
|
|
17
|
+
return updateMcpServerConfig(getGlobalMcpConfigPath(), name, { enabled });
|
|
18
|
+
}
|
|
19
|
+
/** Update only a resolved global/project source, retaining placeholders and unrelated fields. */
|
|
20
|
+
export function updateMcpServerConfig(path, name, patch) {
|
|
18
21
|
const config = readGlobalMcpConfig(path);
|
|
19
22
|
const existing = config.mcpServers?.[name];
|
|
20
23
|
if (existing === undefined)
|
|
21
24
|
return false;
|
|
22
25
|
const next = {
|
|
23
26
|
...config,
|
|
24
|
-
mcpServers: { ...(config.mcpServers ?? {}), [name]: { ...existing,
|
|
27
|
+
mcpServers: { ...(config.mcpServers ?? {}), [name]: { ...existing, ...patch } },
|
|
25
28
|
};
|
|
26
29
|
writeValidatedConfig(path, next);
|
|
27
30
|
return true;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Component, type KeybindingsManager, type SelectItem, type TUI } from "@earendil-works/pi-tui";
|
|
2
|
+
import { type Theme } from "../../../../modes/interactive/theme/theme.ts";
|
|
3
|
+
export interface McpManagerMenu {
|
|
4
|
+
readonly title: string;
|
|
5
|
+
readonly items: SelectItem[];
|
|
6
|
+
readonly details?: string;
|
|
7
|
+
readonly empty?: string;
|
|
8
|
+
readonly confirmLabel: string;
|
|
9
|
+
readonly cancelLabel: string;
|
|
10
|
+
readonly selected?: string;
|
|
11
|
+
}
|
|
12
|
+
/** A focused menu that rebuilds on MCP events, never on a polling timer. */
|
|
13
|
+
export declare class McpManagerView implements Component {
|
|
14
|
+
private content;
|
|
15
|
+
private inputHandler;
|
|
16
|
+
private cancelMenu;
|
|
17
|
+
private rebuild;
|
|
18
|
+
private width;
|
|
19
|
+
private rows;
|
|
20
|
+
private disposed;
|
|
21
|
+
private readonly tui;
|
|
22
|
+
private readonly theme;
|
|
23
|
+
private readonly keybindings;
|
|
24
|
+
constructor(tui: TUI, theme: Theme, keybindings: KeybindingsManager);
|
|
25
|
+
menu(build: () => McpManagerMenu | Promise<McpManagerMenu>, subscribe?: (listener: () => void) => () => void): Promise<string | undefined>;
|
|
26
|
+
status(title: string, message: string): void;
|
|
27
|
+
get columns(): number;
|
|
28
|
+
private setContent;
|
|
29
|
+
handleInput(data: string): void;
|
|
30
|
+
render(width: number): string[];
|
|
31
|
+
invalidate(): void;
|
|
32
|
+
dispose(): void;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=manager-view.d.ts.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Container, SelectList, Spacer, sanitizeTerminalLabel, Text, truncateToWidth, } from "@earendil-works/pi-tui";
|
|
2
|
+
import { DynamicBorder } from "../../../../modes/interactive/components/dynamic-border.js";
|
|
3
|
+
import { keyHint } from "../../../../modes/interactive/components/keybinding-hints.js";
|
|
4
|
+
import { getSelectListTheme } from "../../../../modes/interactive/theme/theme.js";
|
|
5
|
+
/** A focused menu that rebuilds on MCP events, never on a polling timer. */
|
|
6
|
+
export class McpManagerView {
|
|
7
|
+
constructor(tui, theme, keybindings) {
|
|
8
|
+
this.content = new Container();
|
|
9
|
+
this.disposed = false;
|
|
10
|
+
this.tui = tui;
|
|
11
|
+
this.theme = theme;
|
|
12
|
+
this.keybindings = keybindings;
|
|
13
|
+
this.status("MCP servers", "Loading...");
|
|
14
|
+
}
|
|
15
|
+
menu(build, subscribe) {
|
|
16
|
+
if (this.disposed)
|
|
17
|
+
return Promise.resolve(undefined);
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
let generation = 0;
|
|
20
|
+
let settled = false;
|
|
21
|
+
let selected;
|
|
22
|
+
let unsubscribe;
|
|
23
|
+
const finish = (value, error) => {
|
|
24
|
+
if (settled)
|
|
25
|
+
return;
|
|
26
|
+
settled = true;
|
|
27
|
+
generation++;
|
|
28
|
+
unsubscribe?.();
|
|
29
|
+
this.cancelMenu = undefined;
|
|
30
|
+
this.rebuild = undefined;
|
|
31
|
+
this.inputHandler = undefined;
|
|
32
|
+
if (error !== undefined)
|
|
33
|
+
reject(error);
|
|
34
|
+
else
|
|
35
|
+
resolve(value);
|
|
36
|
+
};
|
|
37
|
+
const render = async () => {
|
|
38
|
+
const current = ++generation;
|
|
39
|
+
try {
|
|
40
|
+
const menu = await build();
|
|
41
|
+
if (settled || current !== generation)
|
|
42
|
+
return;
|
|
43
|
+
const list = new SelectList(menu.items.map((item) => ({
|
|
44
|
+
...item,
|
|
45
|
+
label: sanitizeTerminalLabel(item.label || item.value) || "(empty)",
|
|
46
|
+
description: item.description === undefined ? undefined : sanitizeTerminalLabel(item.description),
|
|
47
|
+
})), Math.max(1, Math.min(12, this.tui.terminal.rows - 10)), getSelectListTheme());
|
|
48
|
+
const wanted = selected ?? menu.selected;
|
|
49
|
+
const index = menu.items.findIndex((item) => item.value === wanted);
|
|
50
|
+
if (index !== -1)
|
|
51
|
+
list.setSelectedIndex(index);
|
|
52
|
+
selected = list.getSelectedItem()?.value;
|
|
53
|
+
list.onSelectionChange = (item) => {
|
|
54
|
+
selected = item.value;
|
|
55
|
+
};
|
|
56
|
+
list.onSelect = (item) => finish(item.value);
|
|
57
|
+
list.onCancel = () => finish(undefined);
|
|
58
|
+
const body = [];
|
|
59
|
+
if (menu.details)
|
|
60
|
+
body.push(new Text(this.theme.fg("muted", menu.details.split("\n").map(sanitizeTerminalLabel).join("\n")), 1, 0));
|
|
61
|
+
body.push(new Spacer(1));
|
|
62
|
+
body.push(menu.items.length > 0
|
|
63
|
+
? list
|
|
64
|
+
: new Text(sanitizeTerminalLabel(menu.empty ?? "Nothing to show."), 1, 0));
|
|
65
|
+
const footer = menu.items.length > 0
|
|
66
|
+
? `${keyHint("tui.select.confirm", menu.confirmLabel)} | ${keyHint("tui.select.cancel", menu.cancelLabel)}`
|
|
67
|
+
: keyHint("tui.select.cancel", menu.cancelLabel);
|
|
68
|
+
this.setContent(menu.title, body, footer);
|
|
69
|
+
this.inputHandler = (data) => {
|
|
70
|
+
if (menu.items.length > 0)
|
|
71
|
+
list.handleInput(data);
|
|
72
|
+
else if (this.keybindings.matches(data, "tui.select.cancel"))
|
|
73
|
+
finish(undefined);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (!settled && current === generation)
|
|
78
|
+
finish(undefined, error);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
this.cancelMenu = () => finish(undefined);
|
|
82
|
+
this.rebuild = () => void render();
|
|
83
|
+
this.inputHandler = (data) => {
|
|
84
|
+
if (this.keybindings.matches(data, "tui.select.cancel"))
|
|
85
|
+
finish(undefined);
|
|
86
|
+
};
|
|
87
|
+
unsubscribe = subscribe?.(() => void render());
|
|
88
|
+
void render();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
status(title, message) {
|
|
92
|
+
this.setContent(title, [
|
|
93
|
+
new Spacer(1),
|
|
94
|
+
new Text(this.theme.fg("muted", message.split("\n").map(sanitizeTerminalLabel).join("\n")), 1, 0),
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
get columns() {
|
|
98
|
+
return this.tui.terminal.columns;
|
|
99
|
+
}
|
|
100
|
+
setContent(title, body, footer) {
|
|
101
|
+
if (this.disposed)
|
|
102
|
+
return;
|
|
103
|
+
this.content = new Container();
|
|
104
|
+
this.content.addChild(new DynamicBorder((text) => this.theme.fg("accent", text)));
|
|
105
|
+
this.content.addChild(new Text(this.theme.fg("accent", this.theme.bold(sanitizeTerminalLabel(title))), 1, 0));
|
|
106
|
+
for (const child of body)
|
|
107
|
+
this.content.addChild(child);
|
|
108
|
+
if (footer) {
|
|
109
|
+
this.content.addChild(new Spacer(1));
|
|
110
|
+
this.content.addChild(new Text(footer, 1, 0));
|
|
111
|
+
}
|
|
112
|
+
this.content.addChild(new DynamicBorder((text) => this.theme.fg("accent", text)));
|
|
113
|
+
this.tui.requestRender();
|
|
114
|
+
}
|
|
115
|
+
handleInput(data) {
|
|
116
|
+
this.inputHandler?.(data);
|
|
117
|
+
this.tui.requestRender();
|
|
118
|
+
}
|
|
119
|
+
render(width) {
|
|
120
|
+
if (this.width !== width || this.rows !== this.tui.terminal.rows) {
|
|
121
|
+
this.width = width;
|
|
122
|
+
this.rows = this.tui.terminal.rows;
|
|
123
|
+
this.rebuild?.();
|
|
124
|
+
}
|
|
125
|
+
return this.content.render(width).map((line) => truncateToWidth(line, width, ""));
|
|
126
|
+
}
|
|
127
|
+
invalidate() {
|
|
128
|
+
this.content.invalidate();
|
|
129
|
+
this.rebuild?.();
|
|
130
|
+
}
|
|
131
|
+
dispose() {
|
|
132
|
+
this.disposed = true;
|
|
133
|
+
this.cancelMenu?.();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=manager-view.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "../../types.ts";
|
|
2
|
+
import type { McpService } from "./service.ts";
|
|
3
|
+
type McpManagerCommand = "test" | "reconnect" | "auth" | "logout";
|
|
4
|
+
type Notify = ExtensionCommandContext["ui"]["notify"];
|
|
5
|
+
type RunCommand = (command: McpManagerCommand, name: string, ctx: ExtensionCommandContext, notify?: Notify) => Promise<void>;
|
|
6
|
+
export declare function showMcpManager(ctx: ExtensionCommandContext, pi: ExtensionAPI, service: McpService, run: RunCommand): Promise<void>;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=manager.d.ts.map
|