@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.
Files changed (111) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/bundle/chunks/{app-server-command-MJOYE5ZS.js → app-server-command-WZG7WYMD.js} +1 -1
  3. package/dist/bundle/chunks/{chunk-MJGHZT4N.js → chunk-3A3C5VVO.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-4GUQVY4Z.js → chunk-3YXB7Z7O.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-CB77NQMN.js → chunk-7GVD54RM.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-6EH7QXOG.js → chunk-CTERKXVZ.js} +2 -2
  7. package/dist/bundle/chunks/{chunk-CM5QTRC2.js → chunk-H67EVDWG.js} +1 -1
  8. package/dist/bundle/chunks/chunk-IZOTKUWG.js +2 -0
  9. package/dist/bundle/chunks/{chunk-K4AZKFRB.js → chunk-J5MUMVWB.js} +2 -2
  10. package/dist/bundle/chunks/{chunk-V2VQ4BHG.js → chunk-M2IHGL3M.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-LUWWM7GA.js → chunk-RADHRQ4K.js} +3 -3
  12. package/dist/bundle/chunks/chunk-RRU5LXMS.js +3 -0
  13. package/dist/bundle/chunks/{chunk-BTYFICUC.js → chunk-SB22X6ZR.js} +1 -1
  14. package/dist/bundle/chunks/chunk-ULYOMGXX.js +14 -0
  15. package/dist/bundle/chunks/{chunk-7Q3EY6HH.js → chunk-UMJL74VN.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-5MG76BCM.js → chunk-W6B7SEAK.js} +180 -178
  17. package/dist/bundle/chunks/{cli-main-GWVLFOIP.js → cli-main-T4T5UNRJ.js} +1 -1
  18. package/dist/bundle/chunks/{help-fast-path-JBQLYO7W.js → help-fast-path-44VUGB7H.js} +1 -1
  19. package/dist/bundle/chunks/{host-command-HTYYQBSG.js → host-command-X64D7PT3.js} +1 -1
  20. package/dist/bundle/chunks/{host-lifecycle-XZF6IKKO.js → host-lifecycle-GC3GIR2N.js} +1 -1
  21. package/dist/bundle/chunks/{interactive-mode-YLM2U7IX.js → interactive-mode-ULFSI46Z.js} +1 -1
  22. package/dist/bundle/chunks/{list-tips-JQDZPB6C.js → list-tips-IIRFXBTO.js} +1 -1
  23. package/dist/bundle/chunks/{multi-session-host-KMCBBPTV.js → multi-session-host-VM7WJBC5.js} +1 -1
  24. package/dist/bundle/chunks/{package-manager-cli-3UIJ4RXY.js → package-manager-cli-OAFUQY7E.js} +1 -1
  25. package/dist/bundle/chunks/{rpc-mode-PUYBZG27.js → rpc-mode-XVRJJG3K.js} +1 -1
  26. package/dist/bundle/chunks/{schedule-command-CVAPK2WQ.js → schedule-command-EQ4PQCRP.js} +1 -1
  27. package/dist/bundle/chunks/{session-control-endpoint-THEF3K7M.js → session-control-endpoint-FEGOYWHN.js} +2 -2
  28. package/dist/bundle/chunks/{session-picker-N463VCDI.js → session-picker-R5N2EKUS.js} +1 -1
  29. package/dist/bundle/chunks/session-worker.js +95 -93
  30. package/dist/bundle/cli.js +1 -1
  31. package/dist/bundle/index.js +1 -1
  32. package/dist/bundle/rpc-entry.js +1 -1
  33. package/dist/bundle/runtime-manifest.json +1 -1
  34. package/dist/core/agent-session-runtime.d.ts +9 -0
  35. package/dist/core/agent-session-runtime.js +22 -0
  36. package/dist/core/agent-session.d.ts +12 -1
  37. package/dist/core/agent-session.js +38 -4
  38. package/dist/core/dynamic-prompt/handoff.js +3 -3
  39. package/dist/core/dynamic-prompt/intent-gate.js +1 -1
  40. package/dist/core/dynamic-prompt/style.js +1 -1
  41. package/dist/core/extensions/builtin/btw/panel.d.ts +3 -1
  42. package/dist/core/extensions/builtin/btw/panel.js +14 -9
  43. package/dist/core/extensions/builtin/compaction/context-pipeline.d.ts +5 -0
  44. package/dist/core/extensions/builtin/compaction/context-pipeline.js +1 -0
  45. package/dist/core/extensions/builtin/compaction/index.js +2 -1
  46. package/dist/core/extensions/builtin/compaction/lane-policy.d.ts +7 -0
  47. package/dist/core/extensions/builtin/compaction/lane-policy.js +28 -15
  48. package/dist/core/extensions/builtin/compaction/log.d.ts +2 -0
  49. package/dist/core/extensions/builtin/compaction/log.js +2 -1
  50. package/dist/core/extensions/builtin/goal/cache-warm-renderer.js +14 -8
  51. package/dist/core/extensions/builtin/goal/cache-warm.d.ts +4 -2
  52. package/dist/core/extensions/builtin/goal/monitor-continuation.js +6 -0
  53. package/dist/core/extensions/builtin/mcp/commands.js +21 -26
  54. package/dist/core/extensions/builtin/mcp/config-edit.d.ts +2 -0
  55. package/dist/core/extensions/builtin/mcp/config-edit.js +5 -2
  56. package/dist/core/extensions/builtin/mcp/manager-view.d.ts +34 -0
  57. package/dist/core/extensions/builtin/mcp/manager-view.js +136 -0
  58. package/dist/core/extensions/builtin/mcp/manager.d.ts +8 -0
  59. package/dist/core/extensions/builtin/mcp/manager.js +193 -0
  60. package/dist/core/extensions/builtin/mcp/service.d.ts +3 -1
  61. package/dist/core/extensions/builtin/mcp/service.js +4 -0
  62. package/dist/core/extensions/builtin/permission-system/index.js +4 -0
  63. package/dist/core/extensions/builtin/permission-system/prompt.js +10 -2
  64. package/dist/core/extensions/builtin/permission-system/types.d.ts +3 -1
  65. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.js +2 -2
  66. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js +2 -2
  67. package/dist/core/extensions/builtin/prompt-preset/claude-opus-5-5.js +2 -2
  68. package/dist/core/extensions/builtin/prompt-preset/claude-opus-5.js +2 -2
  69. package/dist/core/extensions/builtin/prompt-preset/claude-sonnet-5-5.js +2 -2
  70. package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.js +3 -3
  71. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js +3 -3
  72. package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.d.ts +2 -2
  73. package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.js +4 -4
  74. package/dist/core/extensions/builtin/prompt-preset/gpt-surface.js +2 -2
  75. package/dist/core/extensions/builtin/prompt-preset/grok-4.5.js +2 -2
  76. package/dist/core/extensions/builtin/prompt-preset/grok-4.6.js +1 -1
  77. package/dist/core/extensions/builtin/prompt-preset/grok-4.7.js +1 -1
  78. package/dist/core/extensions/builtin/prompt-preset/kimi-k3.js +2 -2
  79. package/dist/core/extensions/builtin/rules/rules/finder.js +8 -3
  80. package/dist/core/extensions/types.d.ts +4 -0
  81. package/dist/core/retry-fallback/expansion.js +9 -3
  82. package/dist/core/sdk.js +7 -3
  83. package/dist/core/thinking-levels.d.ts +23 -1
  84. package/dist/core/thinking-levels.js +37 -0
  85. package/dist/core/trust-manager.js +50 -0
  86. package/dist/main.js +12 -13
  87. package/dist/modes/interactive/interactive-mode.js +8 -0
  88. package/dist/modes/interactive/session-control-endpoint.js +33 -8
  89. package/dist/modes/interactive/session-control-registry.js +3 -1
  90. package/dist/modes/interactive/session-control-wake.d.ts +11 -5
  91. package/dist/modes/interactive/session-control-wake.js +38 -16
  92. package/dist/modes/rpc/connection-handler.d.ts +5 -0
  93. package/dist/modes/rpc/connection-handler.js +41 -5
  94. package/dist/modes/rpc/custom-capability.d.ts +6 -0
  95. package/dist/modes/rpc/custom-capability.js +6 -0
  96. package/dist/modes/rpc/host-daemon-paths.d.ts +5 -4
  97. package/dist/modes/rpc/host-daemon-paths.js +17 -9
  98. package/dist/modes/rpc/host-daemon-registration.d.ts +6 -1
  99. package/dist/modes/rpc/host-daemon-registration.js +6 -2
  100. package/dist/modes/rpc/host-session-control.js +6 -4
  101. package/dist/modes/rpc/rpc-client.d.ts +5 -0
  102. package/dist/modes/rpc/rpc-client.js +7 -0
  103. package/dist/modes/rpc/rpc-mode.js +1 -1
  104. package/dist/modes/rpc/rpc-types.d.ts +14 -0
  105. package/docs/json.md +3 -0
  106. package/docs/mcp.md +16 -1
  107. package/docs/rpc.md +49 -1
  108. package/package.json +6 -6
  109. package/dist/bundle/chunks/chunk-6SMU7ANE.js +0 -3
  110. package/dist/bundle/chunks/chunk-VJJ53ORU.js +0 -2
  111. 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
- // Per-cwd cache is the intended contract (pinned by lane-policy.test.ts):
80
- // resumeMode is read once per cwd. A mid-session switch takes effect on
81
- // the next cwd or session.
82
- if (cachedCwd !== context.cwd) {
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} · ${formatExpectedWake(data.dueAtMs, data.waitedMs ?? data.delayMs)} · ${wakeSources}`;
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
- return "Woke on schedule to keep pursuing the goal.";
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 tokens = `~${formatWarmTokenCount(cache.cachedTokens)} tokens`;
76
+ const body = `Prior turn: ~${formatWarmTokenCount(cache.cachedTokens)} cache-read/write tokens (cumulative)`;
70
77
  if (cache.cacheLifetime === "best-effort") {
71
- return `${tokens} were cached after the prior turn · provider caching is best-effort, with no expiry to beat`;
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 `${tokens} were cached after the prior turn · prompt-cache TTL may have elapsed before this wake`;
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)} saved vs a cold re-read`
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
- /** Tokens sitting warm in the provider prompt cache after the last turn. */
52
+ /** Cumulative cache-read/write tokens reported across the prior agent run's requests, not unique context size. */
53
53
  readonly cachedTokens: number;
54
- /** Estimated USD saved by re-reading those tokens from cache instead of paying a cold input read. */
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(rawArgs, ctx, pi, service) {
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
- await showPanel(ctx, service);
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
- ctx.ui.notify(`MCP test ${name} ok (${elapsedMs}ms): ${result.tools.length} tools`);
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
- ctx.ui.notify(`MCP test ${name} failed (${elapsedMs}ms): ${message}`, "error");
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
- ctx.ui.notify(`MCP reconnect ${name} connected`);
153
+ notify(`MCP reconnect ${name} connected`);
159
154
  }
160
155
  catch (error) {
161
156
  const message = error instanceof Error ? error.message : String(error);
162
- ctx.ui.notify(`MCP reconnect ${name} failed: ${message}`, "error");
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
- ctx.ui.notify(`Unknown MCP server: ${name || "<missing>"}\nKnown MCP servers: ${known || "(none)"}`, "error");
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
- const path = getGlobalMcpConfigPath();
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, enabled } },
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