@oh-my-pi/pi-coding-agent 16.1.6 → 16.1.8
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 +52 -0
- package/dist/cli.js +3160 -3139
- package/dist/types/cli/session-picker.d.ts +0 -1
- package/dist/types/cli/tiny-models-cli.d.ts +2 -0
- package/dist/types/collab/protocol.d.ts +20 -1
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +57 -6
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +5 -1
- package/dist/types/internal-urls/filesystem-resource.d.ts +9 -0
- package/dist/types/mcp/loader.d.ts +3 -2
- package/dist/types/mcp/manager.d.ts +4 -3
- package/dist/types/mcp/startup-events.d.ts +21 -4
- package/dist/types/mnemopi/config.d.ts +1 -0
- package/dist/types/modes/components/agent-hub.d.ts +1 -0
- package/dist/types/modes/components/agent-transcript-viewer.d.ts +1 -0
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/btw-panel.d.ts +2 -0
- package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +0 -2
- package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/controllers/streaming-reveal.d.ts +3 -2
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +5 -3
- package/dist/types/session/session-history-format.d.ts +25 -0
- package/dist/types/session/snapcompact-inline.d.ts +1 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +7 -4
- package/dist/types/slash-commands/types.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +1 -0
- package/dist/types/tiny/models.d.ts +11 -7
- package/dist/types/tools/todo.d.ts +1 -0
- package/dist/types/utils/thinking-display.d.ts +2 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +104 -0
- package/src/advisor/runtime.ts +38 -2
- package/src/cli/session-picker.ts +1 -2
- package/src/cli/tiny-models-cli.ts +7 -2
- package/src/collab/guest.ts +172 -20
- package/src/collab/host.ts +47 -5
- package/src/collab/protocol.ts +16 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +59 -5
- package/src/edit/renderer.ts +8 -12
- package/src/extensibility/legacy-pi-ai-shim.ts +16 -4
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/filesystem-resource.ts +34 -0
- package/src/internal-urls/local-protocol.ts +7 -1
- package/src/internal-urls/memory-protocol.ts +5 -1
- package/src/internal-urls/skill-protocol.ts +20 -4
- package/src/internal-urls/vault-protocol.ts +5 -2
- package/src/main.ts +8 -8
- package/src/mcp/loader.ts +4 -3
- package/src/mcp/manager.ts +35 -15
- package/src/mcp/startup-events.ts +106 -11
- package/src/mnemopi/config.ts +2 -0
- package/src/mnemopi/state.ts +3 -1
- package/src/modes/components/agent-hub.ts +4 -0
- package/src/modes/components/agent-transcript-viewer.ts +2 -0
- package/src/modes/components/assistant-message.ts +217 -18
- package/src/modes/components/btw-panel.ts +15 -3
- package/src/modes/components/chat-transcript-builder.ts +8 -2
- package/src/modes/components/model-selector.ts +72 -9
- package/src/modes/components/omfg-panel.ts +1 -1
- package/src/modes/components/session-selector.ts +4 -9
- package/src/modes/components/snapcompact-shape-preview.ts +1 -1
- package/src/modes/components/tool-execution.ts +10 -2
- package/src/modes/controllers/btw-controller.ts +32 -7
- package/src/modes/controllers/event-controller.ts +24 -4
- package/src/modes/controllers/input-controller.ts +43 -21
- package/src/modes/controllers/selector-controller.ts +23 -13
- package/src/modes/controllers/streaming-reveal.ts +78 -20
- package/src/modes/controllers/todo-command-controller.ts +9 -10
- package/src/modes/interactive-mode.ts +83 -8
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/advisor/system.md +1 -1
- package/src/prompts/system/side-channel-no-tools.md +3 -0
- package/src/prompts/system/system-prompt.md +164 -156
- package/src/sdk.ts +12 -8
- package/src/session/agent-session.ts +349 -84
- package/src/session/history-storage.ts +15 -16
- package/src/session/session-history-format.ts +41 -1
- package/src/session/snapcompact-inline.ts +9 -6
- package/src/slash-commands/builtin-registry.ts +147 -21
- package/src/slash-commands/helpers/todo.ts +12 -6
- package/src/slash-commands/types.ts +2 -0
- package/src/system-prompt.ts +6 -11
- package/src/tiny/models.ts +7 -3
- package/src/tiny/title-client.ts +8 -2
- package/src/tiny/worker.ts +1 -0
- package/src/tools/search.ts +10 -1
- package/src/tools/sqlite-reader.ts +59 -3
- package/src/tools/todo.ts +6 -0
- package/src/tools/write.ts +4 -6
- package/src/utils/thinking-display.ts +78 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import type { InternalResource } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Builds a text resource for a filesystem directory resolved by an internal URL handler.
|
|
6
|
+
*
|
|
7
|
+
* The resource is flagged immutable so the read tool never mints hashline edit
|
|
8
|
+
* anchors against a directory listing — only file resources from the same
|
|
9
|
+
* handler stay editable.
|
|
10
|
+
*/
|
|
11
|
+
export async function buildDirectoryResource(
|
|
12
|
+
url: string,
|
|
13
|
+
directoryPath: string,
|
|
14
|
+
notes?: string[],
|
|
15
|
+
): Promise<InternalResource> {
|
|
16
|
+
const entries = await fs.readdir(directoryPath, { withFileTypes: true });
|
|
17
|
+
entries.sort((a, b) => {
|
|
18
|
+
const directoryOrder = Number(b.isDirectory()) - Number(a.isDirectory());
|
|
19
|
+
return directoryOrder || a.name.localeCompare(b.name);
|
|
20
|
+
});
|
|
21
|
+
const content =
|
|
22
|
+
entries.length === 0
|
|
23
|
+
? "(empty directory)"
|
|
24
|
+
: entries.map(e => `${e.name}${e.isDirectory() ? "/" : ""}`).join("\n");
|
|
25
|
+
return {
|
|
26
|
+
url,
|
|
27
|
+
content,
|
|
28
|
+
contentType: "text/plain",
|
|
29
|
+
size: Buffer.byteLength(content, "utf-8"),
|
|
30
|
+
sourcePath: directoryPath,
|
|
31
|
+
immutable: true,
|
|
32
|
+
...(notes ? { notes } : {}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -3,6 +3,7 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { AgentRegistry } from "../registry/agent-registry";
|
|
6
|
+
import { buildDirectoryResource } from "./filesystem-resource";
|
|
6
7
|
import { parseInternalUrl } from "./parse";
|
|
7
8
|
import { validateRelativePath } from "./skill-protocol";
|
|
8
9
|
import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext, UrlCompletion } from "./types";
|
|
@@ -279,8 +280,13 @@ export class LocalProtocolHandler implements ProtocolHandler {
|
|
|
279
280
|
ensureWithinRoot(realTargetPath, resolvedRoot);
|
|
280
281
|
|
|
281
282
|
const stat = await fs.stat(realTargetPath);
|
|
283
|
+
if (stat.isDirectory()) {
|
|
284
|
+
return buildDirectoryResource(url.href, realTargetPath, [
|
|
285
|
+
"Use write path local://<file> to persist large intermediate artifacts across turns.",
|
|
286
|
+
]);
|
|
287
|
+
}
|
|
282
288
|
if (!stat.isFile()) {
|
|
283
|
-
throw new Error(`local:// URL must resolve to a file: ${url.href}`);
|
|
289
|
+
throw new Error(`local:// URL must resolve to a file or directory: ${url.href}`);
|
|
284
290
|
}
|
|
285
291
|
|
|
286
292
|
const content = await Bun.file(realTargetPath).text();
|
|
@@ -3,6 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { getAgentDir, isEnoent } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { getMemoryRoot } from "../memories";
|
|
5
5
|
import { AgentRegistry } from "../registry/agent-registry";
|
|
6
|
+
import { buildDirectoryResource } from "./filesystem-resource";
|
|
6
7
|
import { validateRelativePath } from "./skill-protocol";
|
|
7
8
|
import type { InternalResource, InternalUrl, ProtocolHandler, UrlCompletion } from "./types";
|
|
8
9
|
|
|
@@ -102,8 +103,11 @@ async function tryResolveInRoot(url: InternalUrl, memoryRoot: string): Promise<I
|
|
|
102
103
|
ensureWithinRoot(realTargetPath, resolvedRoot);
|
|
103
104
|
|
|
104
105
|
const stat = await fs.stat(realTargetPath);
|
|
106
|
+
if (stat.isDirectory()) {
|
|
107
|
+
return buildDirectoryResource(url.href, realTargetPath);
|
|
108
|
+
}
|
|
105
109
|
if (!stat.isFile()) {
|
|
106
|
-
throw new Error(`memory:// URL must resolve to a file: ${url.href}`);
|
|
110
|
+
throw new Error(`memory:// URL must resolve to a file or directory: ${url.href}`);
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
const content = await Bun.file(realTargetPath).text();
|
|
@@ -7,8 +7,12 @@
|
|
|
7
7
|
* - skill://<name> - Reads SKILL.md
|
|
8
8
|
* - skill://<name>/<path> - Reads relative path within skill's baseDir
|
|
9
9
|
*/
|
|
10
|
+
import type * as fsTypes from "node:fs";
|
|
11
|
+
import * as fs from "node:fs/promises";
|
|
10
12
|
import * as path from "node:path";
|
|
13
|
+
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
11
14
|
import { getActiveSkills } from "../extensibility/skills";
|
|
15
|
+
import { buildDirectoryResource } from "./filesystem-resource";
|
|
12
16
|
import type { InternalResource, InternalUrl, ProtocolHandler, UrlCompletion } from "./types";
|
|
13
17
|
|
|
14
18
|
function getContentType(filePath: string): InternalResource["contentType"] {
|
|
@@ -71,12 +75,24 @@ export class SkillProtocolHandler implements ProtocolHandler {
|
|
|
71
75
|
targetPath = skill.filePath;
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
let stats: fsTypes.Stats;
|
|
79
|
+
try {
|
|
80
|
+
stats = await fs.stat(targetPath);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (isEnoent(error)) {
|
|
83
|
+
throw new Error(`File not found: ${targetPath}`);
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (stats.isDirectory()) {
|
|
89
|
+
return buildDirectoryResource(url.href, targetPath);
|
|
90
|
+
}
|
|
91
|
+
if (!stats.isFile()) {
|
|
92
|
+
throw new Error(`skill:// URL must resolve to a file or directory: ${url.href}`);
|
|
77
93
|
}
|
|
78
94
|
|
|
79
|
-
const content = await file.text();
|
|
95
|
+
const content = await Bun.file(targetPath).text();
|
|
80
96
|
return {
|
|
81
97
|
url: url.href,
|
|
82
98
|
content,
|
|
@@ -822,7 +822,7 @@ export class VaultProtocolHandler implements ProtocolHandler {
|
|
|
822
822
|
}
|
|
823
823
|
|
|
824
824
|
async #listDir(
|
|
825
|
-
parsed: Extract<ParsedVaultUrl, { kind: "fs-dir" }>,
|
|
825
|
+
parsed: Extract<ParsedVaultUrl, { kind: "fs-dir" | "fs-file" }>,
|
|
826
826
|
context?: ResolveContext,
|
|
827
827
|
): Promise<InternalResource> {
|
|
828
828
|
const { root, targetPath } = await this.#resolveFsTarget(parsed, context);
|
|
@@ -878,8 +878,11 @@ export class VaultProtocolHandler implements ProtocolHandler {
|
|
|
878
878
|
}
|
|
879
879
|
ensureWithinRoot(realTargetPath, root);
|
|
880
880
|
const stat = await fs.promises.stat(realTargetPath);
|
|
881
|
+
if (stat.isDirectory()) {
|
|
882
|
+
return this.#listDir(parsed, context);
|
|
883
|
+
}
|
|
881
884
|
if (!stat.isFile()) {
|
|
882
|
-
throw new Error(`vault:// URL must resolve to a file: ${parsed.url}`);
|
|
885
|
+
throw new Error(`vault:// URL must resolve to a file or directory: ${parsed.url}`);
|
|
883
886
|
}
|
|
884
887
|
|
|
885
888
|
const content = await Bun.file(realTargetPath).text();
|
package/src/main.ts
CHANGED
|
@@ -1056,11 +1056,11 @@ export async function runRootCommand(
|
|
|
1056
1056
|
}
|
|
1057
1057
|
|
|
1058
1058
|
// --print-thoughts (single-shot print mode) must surface reasoning, so un-hide
|
|
1059
|
-
// thinking before the session is built — otherwise a passive
|
|
1059
|
+
// thinking before the session is built — otherwise a passive omitThinking
|
|
1060
1060
|
// setting makes the provider omit summaries and the flag prints nothing. An
|
|
1061
|
-
// explicit --hide-thinking
|
|
1061
|
+
// explicit --hide-thinking block display option still wins for output display.
|
|
1062
1062
|
if (parsedArgs.printThoughts && !isProtocolMode && !isInteractive) {
|
|
1063
|
-
settingsInstance.override("
|
|
1063
|
+
settingsInstance.override("omitThinking", false);
|
|
1064
1064
|
}
|
|
1065
1065
|
// Apply --hide-thinking CLI flag (ephemeral, not persisted)
|
|
1066
1066
|
if (parsedArgs.hideThinking) {
|
|
@@ -1130,21 +1130,21 @@ export async function runRootCommand(
|
|
|
1130
1130
|
if (parsedArgs.resume === true && !parsedArgs.fork) {
|
|
1131
1131
|
const folderSessions = await logger.time("SessionManager.list", SessionManager.list, cwd, parsedArgs.sessionDir);
|
|
1132
1132
|
let preloadedAllSessions: SessionInfo[] | undefined;
|
|
1133
|
-
let startInAllScope = false;
|
|
1134
1133
|
if (folderSessions.length === 0) {
|
|
1135
|
-
//
|
|
1136
|
-
//
|
|
1134
|
+
// Probe globally so we can exit fast when the user has no sessions at
|
|
1135
|
+
// all, but never auto-switch the picker into all-projects scope — that
|
|
1136
|
+
// silently surfaced other projects' history when the cwd was empty
|
|
1137
|
+
// (issue #3099). The preloaded list also makes the user's Tab switch
|
|
1138
|
+
// instant on the way in.
|
|
1137
1139
|
preloadedAllSessions = await logger.time("SessionManager.listAll", SessionManager.listAll);
|
|
1138
1140
|
if (preloadedAllSessions.length === 0) {
|
|
1139
1141
|
writeStartupNotice(parsedArgs, `${chalk.dim("No sessions found")}\n`);
|
|
1140
1142
|
return;
|
|
1141
1143
|
}
|
|
1142
|
-
startInAllScope = true;
|
|
1143
1144
|
}
|
|
1144
1145
|
pauseStartupWatchdog();
|
|
1145
1146
|
const selected = await logger.time("selectSession", selectSession, folderSessions, {
|
|
1146
1147
|
allSessions: preloadedAllSessions,
|
|
1147
|
-
startInAllScope,
|
|
1148
1148
|
});
|
|
1149
1149
|
resumeStartupWatchdog();
|
|
1150
1150
|
if (!selected) {
|
package/src/mcp/loader.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { LoadedCustomTool } from "../extensibility/custom-tools/types";
|
|
|
8
8
|
import { AgentStorage } from "../session/agent-storage";
|
|
9
9
|
import type { AuthStorage } from "../session/auth-storage";
|
|
10
10
|
import { type MCPLoadResult, MCPManager } from "./manager";
|
|
11
|
+
import type { McpConnectionStatusEvent } from "./startup-events";
|
|
11
12
|
import { MCPToolCache } from "./tool-cache";
|
|
12
13
|
|
|
13
14
|
/** Result from loading MCP tools */
|
|
@@ -26,8 +27,8 @@ export interface MCPToolsLoadResult {
|
|
|
26
27
|
|
|
27
28
|
/** Options for loading MCP tools */
|
|
28
29
|
export interface MCPToolsLoadOptions {
|
|
29
|
-
/** Called when
|
|
30
|
-
|
|
30
|
+
/** Called when MCP server connection state changes. */
|
|
31
|
+
onStatus?: (event: McpConnectionStatusEvent) => void;
|
|
31
32
|
/** Whether to load project-level config (default: true) */
|
|
32
33
|
enableProjectConfig?: boolean;
|
|
33
34
|
/** Whether to filter out Exa MCP servers (default: true) */
|
|
@@ -68,7 +69,7 @@ export async function discoverAndLoadMCPTools(cwd: string, options?: MCPToolsLoa
|
|
|
68
69
|
let result: MCPLoadResult;
|
|
69
70
|
try {
|
|
70
71
|
result = await manager.discoverAndConnect({
|
|
71
|
-
|
|
72
|
+
onStatus: options?.onStatus,
|
|
72
73
|
enableProjectConfig: options?.enableProjectConfig,
|
|
73
74
|
filterExa: options?.filterExa,
|
|
74
75
|
filterBrowser: options?.filterBrowser,
|
package/src/mcp/manager.ts
CHANGED
|
@@ -26,13 +26,14 @@ import {
|
|
|
26
26
|
subscribeToResources,
|
|
27
27
|
unsubscribeFromResources,
|
|
28
28
|
} from "./client";
|
|
29
|
-
import { loadAllMCPConfigs, validateServerConfig } from "./config";
|
|
29
|
+
import { type LoadMCPConfigsResult, loadAllMCPConfigs, validateServerConfig } from "./config";
|
|
30
30
|
import {
|
|
31
31
|
lookupMcpOAuthCredential,
|
|
32
32
|
type MCPOAuthCredentialLookup,
|
|
33
33
|
selectMcpOAuthRefreshMaterial,
|
|
34
34
|
} from "./oauth-credentials";
|
|
35
35
|
import { type MCPStoredOAuthCredential, refreshMCPOAuthToken } from "./oauth-flow";
|
|
36
|
+
import type { McpConnectionStatusEvent } from "./startup-events";
|
|
36
37
|
import type { MCPToolDetails } from "./tool-bridge";
|
|
37
38
|
import { DeferredMCPTool, MCPTool } from "./tool-bridge";
|
|
38
39
|
import type { MCPToolCache } from "./tool-cache";
|
|
@@ -148,8 +149,8 @@ export interface MCPDiscoverOptions {
|
|
|
148
149
|
filterExa?: boolean;
|
|
149
150
|
/** Whether to filter out browser MCP servers when builtin browser tool is enabled (default: false) */
|
|
150
151
|
filterBrowser?: boolean;
|
|
151
|
-
/** Called when
|
|
152
|
-
|
|
152
|
+
/** Called when MCP server connection state changes. */
|
|
153
|
+
onStatus?: (event: McpConnectionStatusEvent) => void;
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
/**
|
|
@@ -309,12 +310,20 @@ export class MCPManager {
|
|
|
309
310
|
* Returns tools and any connection errors.
|
|
310
311
|
*/
|
|
311
312
|
async discoverAndConnect(options?: MCPDiscoverOptions): Promise<MCPLoadResult> {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
313
|
+
let loadedConfigs: LoadMCPConfigsResult;
|
|
314
|
+
try {
|
|
315
|
+
loadedConfigs = await loadAllMCPConfigs(this.cwd, {
|
|
316
|
+
enableProjectConfig: options?.enableProjectConfig,
|
|
317
|
+
filterExa: options?.filterExa,
|
|
318
|
+
filterBrowser: options?.filterBrowser,
|
|
319
|
+
});
|
|
320
|
+
} catch (error) {
|
|
321
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
322
|
+
options?.onStatus?.({ type: "failed", serverName: ".mcp.json", error: message });
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
const { configs, exaApiKeys, sources } = loadedConfigs;
|
|
326
|
+
const result = await this.connectServers(configs, sources, options?.onStatus);
|
|
318
327
|
result.exaApiKeys = exaApiKeys;
|
|
319
328
|
return result;
|
|
320
329
|
}
|
|
@@ -326,7 +335,7 @@ export class MCPManager {
|
|
|
326
335
|
async connectServers(
|
|
327
336
|
configs: Record<string, MCPServerConfig>,
|
|
328
337
|
sources: Record<string, SourceMeta>,
|
|
329
|
-
|
|
338
|
+
onStatus?: (event: McpConnectionStatusEvent) => void,
|
|
330
339
|
): Promise<MCPLoadResult> {
|
|
331
340
|
type ConnectionTask = {
|
|
332
341
|
name: string;
|
|
@@ -340,6 +349,8 @@ export class MCPManager {
|
|
|
340
349
|
const allTools: CustomTool<TSchema, MCPToolDetails>[] = [];
|
|
341
350
|
const reportedErrors = new Set<string>();
|
|
342
351
|
let allowBackgroundLogging = false;
|
|
352
|
+
const statusServerNames: string[] = [];
|
|
353
|
+
const validationFailures: Array<{ name: string; message: string }> = [];
|
|
343
354
|
|
|
344
355
|
// Prepare connection tasks
|
|
345
356
|
const connectionTasks: ConnectionTask[] = [];
|
|
@@ -367,10 +378,14 @@ export class MCPManager {
|
|
|
367
378
|
continue;
|
|
368
379
|
}
|
|
369
380
|
|
|
381
|
+
statusServerNames.push(name);
|
|
382
|
+
|
|
370
383
|
// Validate config
|
|
371
384
|
const validationErrors = validateServerConfig(name, config);
|
|
372
385
|
if (validationErrors.length > 0) {
|
|
373
|
-
|
|
386
|
+
const message = validationErrors.join("; ");
|
|
387
|
+
errors.set(name, message);
|
|
388
|
+
validationFailures.push({ name, message });
|
|
374
389
|
reportedErrors.add(name);
|
|
375
390
|
continue;
|
|
376
391
|
}
|
|
@@ -458,20 +473,25 @@ export class MCPManager {
|
|
|
458
473
|
this.#onToolsChanged?.(this.#tools);
|
|
459
474
|
void this.toolCache?.set(name, config, serverTools);
|
|
460
475
|
|
|
476
|
+
onStatus?.({ type: "connected", serverName: name });
|
|
461
477
|
await this.#loadServerResourcesAndPrompts(name, connection);
|
|
462
478
|
})
|
|
463
479
|
.catch(error => {
|
|
464
480
|
if (this.#pendingToolLoads.get(name) !== toolsPromise) return;
|
|
465
481
|
this.#pendingToolLoads.delete(name);
|
|
466
|
-
if (!allowBackgroundLogging || reportedErrors.has(name)) return;
|
|
467
482
|
const message = error instanceof Error ? error.message : String(error);
|
|
483
|
+
onStatus?.({ type: "failed", serverName: name, error: message });
|
|
484
|
+
if (!allowBackgroundLogging || reportedErrors.has(name)) return;
|
|
468
485
|
logger.error("MCP tool load failed", { path: `mcp:${name}`, error: message });
|
|
469
486
|
});
|
|
470
487
|
}
|
|
471
488
|
|
|
472
|
-
// Notify about servers we're connecting to
|
|
473
|
-
if (
|
|
474
|
-
|
|
489
|
+
// Notify about servers we're connecting to, including configs that fail fast.
|
|
490
|
+
if (statusServerNames.length > 0 && onStatus) {
|
|
491
|
+
onStatus({ type: "connecting", serverNames: statusServerNames });
|
|
492
|
+
for (const { name, message } of validationFailures) {
|
|
493
|
+
onStatus({ type: "failed", serverName: name, error: message });
|
|
494
|
+
}
|
|
475
495
|
}
|
|
476
496
|
|
|
477
497
|
if (connectionTasks.length > 0) {
|
|
@@ -1,9 +1,99 @@
|
|
|
1
|
-
|
|
1
|
+
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { replaceTabs, shortenPath, TRUNCATE_LENGTHS, truncateToWidth } from "../tools/render-utils";
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
+
export const MCP_CONNECTION_STATUS_EVENT_CHANNEL = "mcp:connection-status";
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
-
|
|
6
|
+
export type McpConnectionStatusEvent =
|
|
7
|
+
| { type: "connecting"; serverNames: string[] }
|
|
8
|
+
| { type: "connected"; serverName: string }
|
|
9
|
+
| { type: "failed"; serverName: string; error: string };
|
|
10
|
+
|
|
11
|
+
export type McpConnectionStatusSnapshot = {
|
|
12
|
+
pendingServers: readonly string[];
|
|
13
|
+
connectedServers: readonly string[];
|
|
14
|
+
failedServers: readonly { serverName: string; error: string }[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function sanitizeMcpStatusText(value: string, maxWidth: number): string {
|
|
18
|
+
const text = shortenEmbeddedPaths(
|
|
19
|
+
replaceTabs(sanitizeText(value))
|
|
20
|
+
.replace(/[\r\n]+/g, " ")
|
|
21
|
+
.trim(),
|
|
22
|
+
);
|
|
23
|
+
return truncateToWidth(text.length > 0 ? text : "(unnamed)", maxWidth);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sanitizeMcpServerName(serverName: string): string {
|
|
27
|
+
return sanitizeMcpStatusText(serverName, TRUNCATE_LENGTHS.SHORT);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatServerList(serverNames: readonly string[]): string {
|
|
31
|
+
return serverNames.map(sanitizeMcpServerName).join(", ");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatServerCount(count: number): string {
|
|
35
|
+
return count === 1 ? "server" : "servers";
|
|
36
|
+
}
|
|
37
|
+
function sanitizeMcpStatusError(error: string): string {
|
|
38
|
+
return sanitizeMcpStatusText(error, TRUNCATE_LENGTHS.CONTENT);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function shortenEmbeddedPaths(text: string): string {
|
|
42
|
+
return text
|
|
43
|
+
.split(" ")
|
|
44
|
+
.map(segment => {
|
|
45
|
+
const leading = segment.match(/^[("'`[]*/)?.[0] ?? "";
|
|
46
|
+
const trailing = segment.match(/[)"'`,.;:\]]*$/)?.[0] ?? "";
|
|
47
|
+
const end = segment.length - trailing.length;
|
|
48
|
+
if (leading.length >= end) return segment;
|
|
49
|
+
return `${leading}${shortenPath(segment.slice(leading.length, end))}${trailing}`;
|
|
50
|
+
})
|
|
51
|
+
.join(" ");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function formatMCPConnectingMessage(serverNames: readonly string[]): string {
|
|
55
|
+
return `Connecting to MCP servers: ${formatServerList(serverNames)}…`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatFailedServer({ serverName, error }: { serverName: string; error: string }): string {
|
|
59
|
+
return `${sanitizeMcpServerName(serverName)}: ${sanitizeMcpStatusError(error)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function formatMCPConnectionStatusMessage(snapshot: McpConnectionStatusSnapshot): string {
|
|
63
|
+
const { pendingServers, connectedServers, failedServers } = snapshot;
|
|
64
|
+
if (pendingServers.length > 0) {
|
|
65
|
+
if (connectedServers.length === 0 && failedServers.length === 0) {
|
|
66
|
+
return formatMCPConnectingMessage(pendingServers);
|
|
67
|
+
}
|
|
68
|
+
const parts: string[] = [];
|
|
69
|
+
if (connectedServers.length > 0) {
|
|
70
|
+
parts.push(`Connected: ${formatServerList(connectedServers)}.`);
|
|
71
|
+
}
|
|
72
|
+
if (failedServers.length > 0) {
|
|
73
|
+
parts.push(`Failed: ${failedServers.map(formatFailedServer).join("; ")}.`);
|
|
74
|
+
}
|
|
75
|
+
parts.push(`Still connecting: ${formatServerList(pendingServers)}…`);
|
|
76
|
+
return parts.join(" ");
|
|
77
|
+
}
|
|
78
|
+
if (failedServers.length > 0) {
|
|
79
|
+
const failureText = failedServers.map(formatFailedServer).join("; ");
|
|
80
|
+
if (connectedServers.length === 0) {
|
|
81
|
+
return `MCP ${formatServerCount(failedServers.length)} failed to connect: ${failureText}`;
|
|
82
|
+
}
|
|
83
|
+
return `MCP finished with failures. Connected: ${formatServerList(connectedServers)}. Failed: ${failureText}`;
|
|
84
|
+
}
|
|
85
|
+
if (connectedServers.length > 0) {
|
|
86
|
+
return `Connected to MCP ${formatServerCount(connectedServers.length)}: ${formatServerList(connectedServers)}.`;
|
|
87
|
+
}
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isRecord(data: unknown): data is Record<string, unknown> {
|
|
92
|
+
return typeof data === "object" && data !== null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isStringArray(data: unknown): data is string[] {
|
|
96
|
+
return Array.isArray(data) && data.every(item => typeof item === "string");
|
|
7
97
|
}
|
|
8
98
|
|
|
9
99
|
/**
|
|
@@ -11,11 +101,16 @@ export function formatMCPConnectingMessage(serverNames: string[]): string {
|
|
|
11
101
|
* untyped at runtime, so the subscriber verifies the shape before formatting
|
|
12
102
|
* rather than trusting a cast — a malformed emit is ignored instead of throwing.
|
|
13
103
|
*/
|
|
14
|
-
export function
|
|
15
|
-
return
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
104
|
+
export function isMcpConnectionStatusEvent(data: unknown): data is McpConnectionStatusEvent {
|
|
105
|
+
if (!isRecord(data) || typeof data.type !== "string") return false;
|
|
106
|
+
switch (data.type) {
|
|
107
|
+
case "connecting":
|
|
108
|
+
return isStringArray(data.serverNames);
|
|
109
|
+
case "connected":
|
|
110
|
+
return typeof data.serverName === "string";
|
|
111
|
+
case "failed":
|
|
112
|
+
return typeof data.serverName === "string" && typeof data.error === "string";
|
|
113
|
+
default:
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
21
116
|
}
|
package/src/mnemopi/config.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface MnemopiBackendConfig {
|
|
|
26
26
|
autoRetain: boolean;
|
|
27
27
|
polyphonicRecall: boolean;
|
|
28
28
|
enhancedRecall: boolean;
|
|
29
|
+
proactiveLinking: boolean;
|
|
29
30
|
retainEveryNTurns: number;
|
|
30
31
|
recallLimit: number;
|
|
31
32
|
recallContextTurns: number;
|
|
@@ -71,6 +72,7 @@ export function loadMnemopiConfig(settings: Settings, agentDir: string): Mnemopi
|
|
|
71
72
|
autoRetain: settings.get("mnemopi.autoRetain"),
|
|
72
73
|
polyphonicRecall: settings.get("mnemopi.polyphonicRecall"),
|
|
73
74
|
enhancedRecall: settings.get("mnemopi.enhancedRecall"),
|
|
75
|
+
proactiveLinking: settings.get("mnemopi.proactiveLinking"),
|
|
74
76
|
retainEveryNTurns: Math.max(1, Math.floor(settings.get("mnemopi.retainEveryNTurns"))),
|
|
75
77
|
recallLimit: Math.max(1, Math.floor(settings.get("mnemopi.recallLimit"))),
|
|
76
78
|
recallContextTurns: Math.max(1, Math.floor(settings.get("mnemopi.recallContextTurns"))),
|
package/src/mnemopi/state.ts
CHANGED
|
@@ -450,7 +450,8 @@ export class MnemopiSessionState {
|
|
|
450
450
|
// shared bank, then merging recall results while keeping writes project-local.
|
|
451
451
|
function createScopedResources(config: MnemopiBackendConfig): MnemopiScopedResources {
|
|
452
452
|
// Env vars (MNEMOPI_POLYPHONIC_RECALL / MNEMOPI_ENHANCED_RECALL) still override
|
|
453
|
-
// these config-driven defaults inside the core gates.
|
|
453
|
+
// these config-driven defaults inside the core gates. Proactive linking is
|
|
454
|
+
// per-memory instance below so concurrent sessions cannot clobber each other.
|
|
454
455
|
requireMnemopi().configureRecallFeatures({
|
|
455
456
|
polyphonicRecall: config.polyphonicRecall,
|
|
456
457
|
enhancedRecall: config.enhancedRecall,
|
|
@@ -576,6 +577,7 @@ function createMemory(config: MnemopiBackendConfig, bank: string): Mnemopi {
|
|
|
576
577
|
authorType: "agent",
|
|
577
578
|
channelId: bank,
|
|
578
579
|
...providerOptions,
|
|
580
|
+
proactiveLinking: config.proactiveLinking,
|
|
579
581
|
} as ConstructorParameters<typeof Mnemopi>[0]);
|
|
580
582
|
}
|
|
581
583
|
|
|
@@ -157,6 +157,7 @@ export interface AgentHubDeps {
|
|
|
157
157
|
cwd?: string;
|
|
158
158
|
/** Mirrors the main transcript's thinking-block visibility. */
|
|
159
159
|
hideThinkingBlock?: () => boolean;
|
|
160
|
+
proseOnlyThinking?: () => boolean;
|
|
160
161
|
/** Keys toggling tool output expansion (app.tools.expand). */
|
|
161
162
|
expandKeys?: KeyId[];
|
|
162
163
|
/** Focus the main view on this agent's live session (ctx.focusAgentSession). When absent (collab guest, tests), Enter opens the in-hub chat view instead. */
|
|
@@ -194,6 +195,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
194
195
|
#getMessageRenderer: ((customType: string) => MessageRenderer | undefined) | undefined;
|
|
195
196
|
#cwd: string;
|
|
196
197
|
#hideThinkingBlock: (() => boolean) | undefined;
|
|
198
|
+
#proseOnlyThinking: (() => boolean) | undefined;
|
|
197
199
|
#expandKeys: KeyId[];
|
|
198
200
|
#focusAgent: ((id: string) => Promise<void>) | undefined;
|
|
199
201
|
|
|
@@ -223,6 +225,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
223
225
|
this.#getMessageRenderer = deps.getMessageRenderer;
|
|
224
226
|
this.#cwd = deps.cwd ?? getProjectDir();
|
|
225
227
|
this.#hideThinkingBlock = deps.hideThinkingBlock;
|
|
228
|
+
this.#proseOnlyThinking = deps.proseOnlyThinking;
|
|
226
229
|
this.#expandKeys = deps.expandKeys ?? ["ctrl+o"];
|
|
227
230
|
this.#focusAgent = deps.focusAgent;
|
|
228
231
|
|
|
@@ -291,6 +294,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
291
294
|
getMessageRenderer: this.#getMessageRenderer,
|
|
292
295
|
cwd: this.#cwd,
|
|
293
296
|
hideThinkingBlock: this.#hideThinkingBlock,
|
|
297
|
+
proseOnlyThinking: this.#proseOnlyThinking,
|
|
294
298
|
expandKeys: this.#expandKeys,
|
|
295
299
|
hubKeys: this.#hubKeys,
|
|
296
300
|
requestRender: this.#requestRender,
|
|
@@ -50,6 +50,7 @@ export interface AgentTranscriptViewerDeps {
|
|
|
50
50
|
getMessageRenderer?: (customType: string) => MessageRenderer | undefined;
|
|
51
51
|
cwd: string;
|
|
52
52
|
hideThinkingBlock?: () => boolean;
|
|
53
|
+
proseOnlyThinking?: () => boolean;
|
|
53
54
|
expandKeys: KeyId[];
|
|
54
55
|
/** Keys that toggle the whole hub closed (app.agents.hub + app.session.observe). */
|
|
55
56
|
hubKeys: KeyId[];
|
|
@@ -105,6 +106,7 @@ export class AgentTranscriptViewer implements Component {
|
|
|
105
106
|
getMessageRenderer: deps.getMessageRenderer,
|
|
106
107
|
cwd: deps.cwd,
|
|
107
108
|
hideThinkingBlock: deps.hideThinkingBlock,
|
|
109
|
+
proseOnlyThinking: deps.proseOnlyThinking,
|
|
108
110
|
requestRender: deps.requestRender,
|
|
109
111
|
});
|
|
110
112
|
this.#scrollView = new ScrollView([], {
|