@oh-my-pi/pi-coding-agent 15.11.2 → 15.11.4
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 +63 -0
- package/dist/cli.js +365 -305
- package/dist/types/config/api-key-resolver.d.ts +9 -3
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/model-discovery.d.ts +6 -4
- package/dist/types/config/model-registry.d.ts +7 -4
- package/dist/types/config/settings-schema.d.ts +458 -155
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/lsp/format-options.d.ts +32 -0
- package/dist/types/mnemopi/config.d.ts +3 -1
- package/dist/types/mnemopi/state.d.ts +29 -1
- package/dist/types/modes/components/settings-defs.d.ts +9 -2
- package/dist/types/modes/components/settings-selector.d.ts +9 -4
- package/dist/types/modes/components/tool-execution.d.ts +12 -1
- package/dist/types/modes/components/transcript-container.d.ts +12 -0
- package/dist/types/modes/controllers/input-controller.d.ts +9 -1
- package/dist/types/modes/theme/theme.d.ts +23 -3
- package/dist/types/session/agent-session.d.ts +14 -7
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/snapcompact-inline.d.ts +28 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +14 -0
- package/dist/types/system-prompt.d.ts +3 -1
- package/dist/types/task/render.d.ts +16 -6
- package/dist/types/tools/gh.d.ts +3 -0
- package/dist/types/tools/path-utils.d.ts +5 -1
- package/dist/types/tools/render-utils.d.ts +8 -16
- package/dist/types/utils/git.d.ts +1 -1
- package/dist/types/utils/session-color.d.ts +15 -3
- package/dist/types/web/kagi.d.ts +1 -2
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/dist/types/web/search/providers/gemini.d.ts +9 -6
- package/package.json +11 -11
- package/src/auto-thinking/classifier.ts +1 -5
- package/src/commit/model-selection.ts +3 -6
- package/src/config/api-key-resolver.ts +10 -3
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-discovery.ts +60 -46
- package/src/config/model-registry.ts +21 -8
- package/src/config/model-resolver.ts +57 -3
- package/src/config/settings-schema.ts +601 -153
- package/src/eval/completion-bridge.ts +1 -5
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +13 -6
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/issue-pr-protocol.ts +10 -4
- package/src/lsp/clients/lsp-linter-client.ts +2 -10
- package/src/lsp/format-options.ts +119 -0
- package/src/lsp/index.ts +2 -10
- package/src/memories/index.ts +2 -10
- package/src/mnemopi/backend.ts +34 -16
- package/src/mnemopi/config.ts +6 -1
- package/src/mnemopi/state.ts +48 -3
- package/src/modes/components/extensions/inspector-panel.ts +6 -2
- package/src/modes/components/plan-review-overlay.ts +15 -17
- package/src/modes/components/plugin-settings.ts +22 -5
- package/src/modes/components/settings-defs.ts +19 -4
- package/src/modes/components/settings-selector.ts +493 -93
- package/src/modes/components/status-line/component.ts +3 -1
- package/src/modes/components/status-line/segments.ts +3 -1
- package/src/modes/components/tool-execution.ts +69 -12
- package/src/modes/components/transcript-container.ts +26 -0
- package/src/modes/components/tree-selector.ts +16 -6
- package/src/modes/controllers/command-controller.ts +37 -7
- package/src/modes/controllers/event-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +68 -6
- package/src/modes/controllers/selector-controller.ts +81 -61
- package/src/modes/interactive-mode.ts +26 -4
- package/src/modes/rpc/rpc-mode.ts +2 -1
- package/src/modes/shared.ts +2 -0
- package/src/modes/theme/theme.ts +100 -7
- package/src/modes/utils/context-usage.ts +3 -1
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +9 -5
- package/src/prompts/system/personalities/default.md +26 -0
- package/src/prompts/system/personalities/friendly.md +17 -0
- package/src/prompts/system/personalities/pragmatic.md +15 -0
- package/src/prompts/system/snapcompact-system-frames-note.md +1 -0
- package/src/prompts/system/snapcompact-system-stub.md +1 -0
- package/src/prompts/system/snapcompact-toolresult-note.md +1 -0
- package/src/prompts/system/system-prompt.md +5 -22
- package/src/prompts/tools/task.md +3 -3
- package/src/sdk.ts +22 -1
- package/src/session/agent-session.ts +92 -25
- package/src/session/auth-storage.ts +1 -0
- package/src/session/session-dump-format.ts +8 -1
- package/src/session/session-manager.ts +5 -5
- package/src/session/snapcompact-inline.ts +187 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +44 -0
- package/src/slash-commands/helpers/usage-report.ts +24 -3
- package/src/system-prompt.ts +15 -1
- package/src/task/render.ts +29 -19
- package/src/tool-discovery/tool-index.ts +2 -0
- package/src/tools/bash.ts +10 -3
- package/src/tools/eval-render.ts +13 -8
- package/src/tools/gh.ts +39 -1
- package/src/tools/image-gen.ts +114 -78
- package/src/tools/inspect-image.ts +1 -5
- package/src/tools/job.ts +25 -5
- package/src/tools/path-utils.ts +34 -10
- package/src/tools/read.ts +1 -57
- package/src/tools/render-utils.ts +29 -31
- package/src/tools/search.ts +11 -0
- package/src/tools/ssh.ts +3 -3
- package/src/tools/tts.ts +40 -20
- package/src/utils/clipboard.ts +56 -4
- package/src/utils/commit-message-generator.ts +1 -5
- package/src/utils/git.ts +7 -2
- package/src/utils/session-color.ts +83 -9
- package/src/utils/title-generator.ts +1 -1
- package/src/web/kagi.ts +26 -27
- package/src/web/search/providers/codex.ts +42 -40
- package/src/web/search/providers/gemini.ts +42 -22
- package/src/web/search/providers/perplexity.ts +22 -10
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
getOrFetchIssue,
|
|
24
24
|
getOrFetchPr,
|
|
25
25
|
getOrFetchPrDiff,
|
|
26
|
+
githubIssueJsonWithStateReasonFallback,
|
|
26
27
|
type PrDiffFile,
|
|
27
28
|
parsePositiveDecimalInt,
|
|
28
29
|
resolveDefaultRepoMemoized,
|
|
@@ -294,7 +295,7 @@ async function fetchAndRenderList(
|
|
|
294
295
|
const cwd = resolveCwd(context);
|
|
295
296
|
const fields =
|
|
296
297
|
scheme === "issue"
|
|
297
|
-
? ["number", "title", "state", "
|
|
298
|
+
? ["number", "title", "state", "author", "labels", "createdAt", "updatedAt", "url"]
|
|
298
299
|
: [
|
|
299
300
|
"number",
|
|
300
301
|
"title",
|
|
@@ -323,9 +324,14 @@ async function fetchAndRenderList(
|
|
|
323
324
|
if (options.author) args.push("--author", options.author);
|
|
324
325
|
if (options.label) args.push("--label", options.label);
|
|
325
326
|
|
|
326
|
-
const items =
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
const items =
|
|
328
|
+
scheme === "issue"
|
|
329
|
+
? await githubIssueJsonWithStateReasonFallback<Array<IssueListItem>>(cwd, args, context?.signal, {
|
|
330
|
+
repoProvided: true,
|
|
331
|
+
})
|
|
332
|
+
: await git.github.json<Array<PrListItem>>(cwd, args, context?.signal, {
|
|
333
|
+
repoProvided: true,
|
|
334
|
+
});
|
|
329
335
|
const header =
|
|
330
336
|
scheme === "issue"
|
|
331
337
|
? `# Issues in ${repo} (${options.state}, up to ${options.limit})`
|
|
@@ -4,18 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { getOrCreateClient, notifySaved, sendRequest, syncContent } from "../../lsp/client";
|
|
6
6
|
import { applyTextEditsToString } from "../../lsp/edits";
|
|
7
|
+
import { resolveFormatOptions } from "../../lsp/format-options";
|
|
7
8
|
import type { Diagnostic, LinterClient, LspClient, ServerConfig, TextEdit } from "../../lsp/types";
|
|
8
9
|
import { fileToUri } from "../../lsp/utils";
|
|
9
10
|
|
|
10
|
-
/** Default formatting options for LSP */
|
|
11
|
-
const DEFAULT_FORMAT_OPTIONS = {
|
|
12
|
-
tabSize: 3,
|
|
13
|
-
insertSpaces: true,
|
|
14
|
-
trimTrailingWhitespace: true,
|
|
15
|
-
insertFinalNewline: true,
|
|
16
|
-
trimFinalNewlines: true,
|
|
17
|
-
};
|
|
18
|
-
|
|
19
11
|
/**
|
|
20
12
|
* LSP-based linter client implementation.
|
|
21
13
|
* Wraps the existing LSP client infrastructure.
|
|
@@ -56,7 +48,7 @@ export class LspLinterClient implements LinterClient {
|
|
|
56
48
|
// Request formatting
|
|
57
49
|
const edits = (await sendRequest(client, "textDocument/formatting", {
|
|
58
50
|
textDocument: { uri },
|
|
59
|
-
options:
|
|
51
|
+
options: resolveFormatOptions(filePath, content),
|
|
60
52
|
})) as TextEdit[] | null;
|
|
61
53
|
|
|
62
54
|
if (!edits || edits.length === 0) {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-file LSP `FormattingOptions` resolution.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the historical hardcoded `{ tabSize: 3, insertSpaces: true }` default
|
|
5
|
+
* that fed every `textDocument/formatting` request — it silently re-indented
|
|
6
|
+
* 2-space YAML (and any LSP-formatted file) on every write/edit (issue #2329).
|
|
7
|
+
*
|
|
8
|
+
* Precedence, highest to lowest:
|
|
9
|
+
* 1. `.editorconfig` in the file's chain (`indent_style`, `indent_size`, `tab_width`).
|
|
10
|
+
* 2. Indent detected from the file content the agent is about to write.
|
|
11
|
+
* 3. Hardcoded fallback — 2 spaces, matching the dominant convention for YAML,
|
|
12
|
+
* JSON, JS/TS, Python (PEP 8 is 4 but most LSP servers honour their own
|
|
13
|
+
* defaults when ours don't disagree), and most config formats. The previous
|
|
14
|
+
* `3` default was an unusual stride that actively damaged every file with
|
|
15
|
+
* a 2/4-space convention.
|
|
16
|
+
*/
|
|
17
|
+
import { getEditorConfigFormatting } from "@oh-my-pi/pi-utils";
|
|
18
|
+
|
|
19
|
+
/** Subset of the LSP `FormattingOptions` we send. */
|
|
20
|
+
export interface LspFormattingOptions {
|
|
21
|
+
tabSize: number;
|
|
22
|
+
insertSpaces: boolean;
|
|
23
|
+
trimTrailingWhitespace: boolean;
|
|
24
|
+
insertFinalNewline: boolean;
|
|
25
|
+
trimFinalNewlines: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Sensible fallback when neither `.editorconfig` nor file content pins the indent. */
|
|
29
|
+
const FALLBACK_TAB_SIZE = 2;
|
|
30
|
+
const FALLBACK_INSERT_SPACES = true;
|
|
31
|
+
|
|
32
|
+
/** Static flags we always pass — these have no per-file analogue and match common formatter expectations. */
|
|
33
|
+
const TRIM_OPTIONS = {
|
|
34
|
+
trimTrailingWhitespace: true,
|
|
35
|
+
insertFinalNewline: true,
|
|
36
|
+
trimFinalNewlines: true,
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
interface DetectedIndent {
|
|
40
|
+
tabSize?: number;
|
|
41
|
+
insertSpaces?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Sniff `insertSpaces` and the indent unit from `content`.
|
|
46
|
+
*
|
|
47
|
+
* Walks the buffer once: the first indented line decides spaces vs tabs; for
|
|
48
|
+
* space indents, the GCD of all space-indent widths gives the stride (so a
|
|
49
|
+
* 2/4/6 file reports `2`, a 4/8 file reports `4`). Returns `undefined` for any
|
|
50
|
+
* field the content does not pin so a higher-precedence override (editorconfig)
|
|
51
|
+
* can win without being overwritten by sniffing noise.
|
|
52
|
+
*/
|
|
53
|
+
export function detectIndentFromContent(content: string): DetectedIndent {
|
|
54
|
+
if (content.length === 0) return {};
|
|
55
|
+
|
|
56
|
+
let insertSpaces: boolean | undefined;
|
|
57
|
+
let unit = 0;
|
|
58
|
+
|
|
59
|
+
// Split is the cheapest reliable line walk on arbitrary text; the
|
|
60
|
+
// per-line regex matches are O(leading whitespace) so total cost is
|
|
61
|
+
// linear in the file's indented prefix bytes.
|
|
62
|
+
for (const line of content.split("\n")) {
|
|
63
|
+
// Skip blank/whitespace-only lines — they carry no indent signal.
|
|
64
|
+
if (line.length === 0 || line.trim().length === 0) continue;
|
|
65
|
+
|
|
66
|
+
const first = line[0];
|
|
67
|
+
if (first !== " " && first !== "\t") continue;
|
|
68
|
+
|
|
69
|
+
if (insertSpaces === undefined) {
|
|
70
|
+
insertSpaces = first === " ";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Tab-indented file: the unit is one tab per level; tabSize is a
|
|
74
|
+
// display concern, leave it to caller defaults / editorconfig.
|
|
75
|
+
if (first === "\t") continue;
|
|
76
|
+
|
|
77
|
+
// Space-indented: count the leading spaces (stop at first tab to avoid
|
|
78
|
+
// mixing). GCD across non-zero widths converges on the stride.
|
|
79
|
+
let n = 0;
|
|
80
|
+
while (n < line.length && line[n] === " ") n++;
|
|
81
|
+
if (n === 0) continue;
|
|
82
|
+
unit = unit === 0 ? n : gcd(unit, n);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const result: DetectedIndent = {};
|
|
86
|
+
if (insertSpaces !== undefined) result.insertSpaces = insertSpaces;
|
|
87
|
+
if (unit > 0 && insertSpaces === true) result.tabSize = unit;
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function gcd(a: number, b: number): number {
|
|
92
|
+
let x = a;
|
|
93
|
+
let y = b;
|
|
94
|
+
while (y !== 0) {
|
|
95
|
+
const t = y;
|
|
96
|
+
y = x % y;
|
|
97
|
+
x = t;
|
|
98
|
+
}
|
|
99
|
+
return x;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the `FormattingOptions` payload for a `textDocument/formatting` request
|
|
104
|
+
* targeting `filePath` with `content`.
|
|
105
|
+
*
|
|
106
|
+
* The two fields that actually affect on-disk bytes (`tabSize`, `insertSpaces`)
|
|
107
|
+
* are layered: editorconfig wins, then content sniffing, then the fallback.
|
|
108
|
+
* Trim/final-newline flags are static.
|
|
109
|
+
*/
|
|
110
|
+
export function resolveFormatOptions(filePath: string, content: string): LspFormattingOptions {
|
|
111
|
+
const fromConfig = getEditorConfigFormatting(filePath);
|
|
112
|
+
const detected = detectIndentFromContent(content);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
tabSize: fromConfig.tabSize ?? detected.tabSize ?? FALLBACK_TAB_SIZE,
|
|
116
|
+
insertSpaces: fromConfig.insertSpaces ?? detected.insertSpaces ?? FALLBACK_INSERT_SPACES,
|
|
117
|
+
...TRIM_OPTIONS,
|
|
118
|
+
};
|
|
119
|
+
}
|
package/src/lsp/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
flattenWorkspaceTextEdits,
|
|
40
40
|
rangesOverlap,
|
|
41
41
|
} from "./edits";
|
|
42
|
+
import { resolveFormatOptions } from "./format-options";
|
|
42
43
|
import { detectLspmux } from "./lspmux";
|
|
43
44
|
import {
|
|
44
45
|
type CodeAction,
|
|
@@ -779,15 +780,6 @@ export enum FileFormatResult {
|
|
|
779
780
|
FORMATTED = "formatted",
|
|
780
781
|
}
|
|
781
782
|
|
|
782
|
-
/** Default formatting options for LSP */
|
|
783
|
-
const DEFAULT_FORMAT_OPTIONS = {
|
|
784
|
-
tabSize: 3,
|
|
785
|
-
insertSpaces: true,
|
|
786
|
-
trimTrailingWhitespace: true,
|
|
787
|
-
insertFinalNewline: true,
|
|
788
|
-
trimFinalNewlines: true,
|
|
789
|
-
};
|
|
790
|
-
|
|
791
783
|
/**
|
|
792
784
|
* Format content using LSP or custom linter client.
|
|
793
785
|
*
|
|
@@ -834,7 +826,7 @@ async function formatContent(
|
|
|
834
826
|
"textDocument/formatting",
|
|
835
827
|
{
|
|
836
828
|
textDocument: { uri },
|
|
837
|
-
options:
|
|
829
|
+
options: resolveFormatOptions(absolutePath, content),
|
|
838
830
|
},
|
|
839
831
|
signal,
|
|
840
832
|
)) as TextEdit[] | null;
|
package/src/memories/index.ts
CHANGED
|
@@ -274,11 +274,7 @@ async function runPhase1(options: {
|
|
|
274
274
|
const result = await runStage1Job({
|
|
275
275
|
claim,
|
|
276
276
|
model: phase1Model,
|
|
277
|
-
apiKey: modelRegistry.resolver(phase1Model.
|
|
278
|
-
sessionId: session.sessionId,
|
|
279
|
-
baseUrl: phase1Model.baseUrl,
|
|
280
|
-
modelId: phase1Model.id,
|
|
281
|
-
}),
|
|
277
|
+
apiKey: modelRegistry.resolver(phase1Model, session.sessionId),
|
|
282
278
|
modelMaxTokens: computeModelTokenBudget(phase1Model, config),
|
|
283
279
|
config,
|
|
284
280
|
metadata: session.agent?.metadataForProvider(phase1Model.provider),
|
|
@@ -435,11 +431,7 @@ async function runPhase2(options: {
|
|
|
435
431
|
const consolidated = await runConsolidationModel({
|
|
436
432
|
memoryRoot,
|
|
437
433
|
model: phase2Model,
|
|
438
|
-
apiKey: modelRegistry.resolver(phase2Model.
|
|
439
|
-
sessionId: session.sessionId,
|
|
440
|
-
baseUrl: phase2Model.baseUrl,
|
|
441
|
-
modelId: phase2Model.id,
|
|
442
|
-
}),
|
|
434
|
+
apiKey: modelRegistry.resolver(phase2Model, session.sessionId),
|
|
443
435
|
metadata: session.agent?.metadataForProvider(phase2Model.provider),
|
|
444
436
|
});
|
|
445
437
|
await applyConsolidation(memoryRoot, consolidated);
|
package/src/mnemopi/backend.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { rm } from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { completeSimple } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import { type ApiKeyResolver, completeSimple } from "@oh-my-pi/pi-ai";
|
|
4
|
+
import { hostMatchesUrl } from "@oh-my-pi/pi-catalog/hosts";
|
|
4
5
|
import type { Mnemopi } from "@oh-my-pi/pi-mnemopi";
|
|
5
6
|
import type * as MnemopiDiagnoseNs from "@oh-my-pi/pi-mnemopi/diagnose";
|
|
6
7
|
import type { DiagnosticSummary } from "@oh-my-pi/pi-mnemopi/diagnose";
|
|
@@ -82,7 +83,7 @@ export const mnemopiBackend: MemoryBackend = {
|
|
|
82
83
|
hasRecalledForFirstTurn: true,
|
|
83
84
|
}),
|
|
84
85
|
);
|
|
85
|
-
previous?.dispose();
|
|
86
|
+
await previous?.dispose();
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
88
89
|
|
|
@@ -91,7 +92,7 @@ export const mnemopiBackend: MemoryBackend = {
|
|
|
91
92
|
await Promise.all([loadMnemopi(), loadMnemopiCore()]);
|
|
92
93
|
const state = new MnemopiSessionState({ sessionId, config, session });
|
|
93
94
|
const previous = setMnemopiSessionState(session, state);
|
|
94
|
-
previous?.dispose();
|
|
95
|
+
await previous?.dispose();
|
|
95
96
|
state.attachSessionListeners();
|
|
96
97
|
} catch (error) {
|
|
97
98
|
logger.warn("Mnemopi: backend startup failed; memory backend inert.", { error: String(error) });
|
|
@@ -115,7 +116,7 @@ export const mnemopiBackend: MemoryBackend = {
|
|
|
115
116
|
|
|
116
117
|
async clear(agentDir, _cwd, session): Promise<void> {
|
|
117
118
|
const previous = session ? setMnemopiSessionState(session, undefined) : undefined;
|
|
118
|
-
previous?.dispose();
|
|
119
|
+
await previous?.dispose({ consolidate: false });
|
|
119
120
|
const config = previous?.config ?? (session ? loadMnemopiConfig(session.settings, agentDir) : undefined);
|
|
120
121
|
if (!config) return;
|
|
121
122
|
await loadMnemopiCore();
|
|
@@ -136,11 +137,7 @@ export const mnemopiBackend: MemoryBackend = {
|
|
|
136
137
|
state = new MnemopiSessionState({ sessionId: session.sessionId, config, session });
|
|
137
138
|
setMnemopiSessionState(session, state);
|
|
138
139
|
}
|
|
139
|
-
await state?.
|
|
140
|
-
// Drain the background fact extraction scheduled by the final retain
|
|
141
|
-
// before the process can exit, otherwise the last turn's facts are lost.
|
|
142
|
-
await state?.memory.flushExtractions();
|
|
143
|
-
state?.memory.sleepAllSessions(false);
|
|
140
|
+
await state?.consolidate();
|
|
144
141
|
} catch (error) {
|
|
145
142
|
logger.warn("Mnemopi: enqueue failed.", { error: String(error) });
|
|
146
143
|
}
|
|
@@ -437,6 +434,25 @@ async function loadMnemopiConfigWithProviders(
|
|
|
437
434
|
return config;
|
|
438
435
|
}
|
|
439
436
|
|
|
437
|
+
/**
|
|
438
|
+
* When mnemopi targets OpenRouter (its default embedding host) without a
|
|
439
|
+
* user-pinned key, hand it the central {@link ApiKeyResolver} so requests pick
|
|
440
|
+
* up AuthStorage credentials, force-refresh on 401, and rotate across sibling
|
|
441
|
+
* keys. Returns undefined when the URL points elsewhere or when no OpenRouter
|
|
442
|
+
* credential exists, preserving mnemopi's env-key fallback and its
|
|
443
|
+
* "no key -> API embeddings unavailable" gating.
|
|
444
|
+
*/
|
|
445
|
+
async function openrouterKeyResolver(
|
|
446
|
+
modelRegistry: ModelRegistry,
|
|
447
|
+
sessionId: string,
|
|
448
|
+
baseUrl: string | undefined,
|
|
449
|
+
): Promise<ApiKeyResolver | undefined> {
|
|
450
|
+
if (baseUrl !== undefined && !hostMatchesUrl(baseUrl, "openrouter")) return undefined;
|
|
451
|
+
const key = await modelRegistry.getApiKeyForProvider("openrouter", sessionId);
|
|
452
|
+
if (key === undefined || key === "") return undefined;
|
|
453
|
+
return modelRegistry.resolver("openrouter", { sessionId });
|
|
454
|
+
}
|
|
455
|
+
|
|
440
456
|
async function resolveMnemopiProviderOptions(
|
|
441
457
|
config: MnemopiBackendConfig,
|
|
442
458
|
settings: MemoryBackendStartOptions["settings"],
|
|
@@ -447,7 +463,9 @@ async function resolveMnemopiProviderOptions(
|
|
|
447
463
|
noEmbeddings: config.providerOptions.noEmbeddings,
|
|
448
464
|
embeddingModel: config.providerOptions.embeddingModel,
|
|
449
465
|
embeddingApiUrl: config.providerOptions.embeddingApiUrl,
|
|
450
|
-
embeddingApiKey:
|
|
466
|
+
embeddingApiKey:
|
|
467
|
+
config.providerOptions.embeddingApiKey ??
|
|
468
|
+
(await openrouterKeyResolver(modelRegistry, sessionId, config.providerOptions.embeddingApiUrl)),
|
|
451
469
|
llm: false,
|
|
452
470
|
};
|
|
453
471
|
|
|
@@ -473,7 +491,11 @@ async function resolveMnemopiProviderOptions(
|
|
|
473
491
|
...base,
|
|
474
492
|
llm: {
|
|
475
493
|
baseUrl: config.llmBaseUrl,
|
|
476
|
-
apiKey:
|
|
494
|
+
apiKey:
|
|
495
|
+
config.llmApiKey ??
|
|
496
|
+
(config.llmBaseUrl === undefined
|
|
497
|
+
? undefined
|
|
498
|
+
: await openrouterKeyResolver(modelRegistry, sessionId, config.llmBaseUrl)),
|
|
477
499
|
model: config.llmModel,
|
|
478
500
|
},
|
|
479
501
|
};
|
|
@@ -503,11 +525,7 @@ async function resolveMnemopiProviderOptions(
|
|
|
503
525
|
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
|
|
504
526
|
},
|
|
505
527
|
{
|
|
506
|
-
apiKey: modelRegistry.resolver(model
|
|
507
|
-
sessionId,
|
|
508
|
-
baseUrl: model.baseUrl,
|
|
509
|
-
modelId: model.id,
|
|
510
|
-
}),
|
|
528
|
+
apiKey: modelRegistry.resolver(model, sessionId),
|
|
511
529
|
maxTokens: opts?.maxTokens,
|
|
512
530
|
temperature: opts?.temperature,
|
|
513
531
|
},
|
package/src/mnemopi/config.ts
CHANGED
|
@@ -10,7 +10,7 @@ export type MnemopiScoping = "global" | "per-project" | "per-project-tagged";
|
|
|
10
10
|
|
|
11
11
|
export type MnemopiProviderOptions = Pick<
|
|
12
12
|
MnemopiOptions,
|
|
13
|
-
"noEmbeddings" | "embeddingModel" | "embeddingApiUrl" | "embeddingApiKey" | "llm"
|
|
13
|
+
"noEmbeddings" | "embeddingModel" | "embeddingApiUrl" | "embeddingApiKey" | "llm" | "debug"
|
|
14
14
|
>;
|
|
15
15
|
|
|
16
16
|
export interface MnemopiBackendConfig {
|
|
@@ -23,6 +23,8 @@ export interface MnemopiBackendConfig {
|
|
|
23
23
|
scoping?: MnemopiScoping;
|
|
24
24
|
autoRecall: boolean;
|
|
25
25
|
autoRetain: boolean;
|
|
26
|
+
polyphonicRecall: boolean;
|
|
27
|
+
enhancedRecall: boolean;
|
|
26
28
|
retainEveryNTurns: number;
|
|
27
29
|
recallLimit: number;
|
|
28
30
|
recallContextTurns: number;
|
|
@@ -52,6 +54,8 @@ export function loadMnemopiConfig(settings: Settings, agentDir: string): Mnemopi
|
|
|
52
54
|
scoping,
|
|
53
55
|
autoRecall: settings.get("mnemopi.autoRecall"),
|
|
54
56
|
autoRetain: settings.get("mnemopi.autoRetain"),
|
|
57
|
+
polyphonicRecall: settings.get("mnemopi.polyphonicRecall"),
|
|
58
|
+
enhancedRecall: settings.get("mnemopi.enhancedRecall"),
|
|
55
59
|
retainEveryNTurns: Math.max(1, Math.floor(settings.get("mnemopi.retainEveryNTurns"))),
|
|
56
60
|
recallLimit: Math.max(1, Math.floor(settings.get("mnemopi.recallLimit"))),
|
|
57
61
|
recallContextTurns: Math.max(1, Math.floor(settings.get("mnemopi.recallContextTurns"))),
|
|
@@ -60,6 +64,7 @@ export function loadMnemopiConfig(settings: Settings, agentDir: string): Mnemopi
|
|
|
60
64
|
debug: settings.get("mnemopi.debug"),
|
|
61
65
|
providerOptions: {
|
|
62
66
|
noEmbeddings: settings.get("mnemopi.noEmbeddings"),
|
|
67
|
+
debug: settings.get("mnemopi.debug"),
|
|
63
68
|
embeddingModel: settings.get("mnemopi.embeddingModel"),
|
|
64
69
|
embeddingApiUrl: settings.get("mnemopi.embeddingApiUrl"),
|
|
65
70
|
embeddingApiKey: settings.get("mnemopi.embeddingApiKey"),
|
package/src/mnemopi/state.ts
CHANGED
|
@@ -370,18 +370,63 @@ export class MnemopiSessionState {
|
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
|
|
373
|
+
/**
|
|
374
|
+
* Drain in-flight fact extraction and run beam consolidation on every owned
|
|
375
|
+
* bank, after capturing the current transcript. Mirrors the manual
|
|
376
|
+
* `/memory enqueue` slash command, but stops short of closing the DBs so
|
|
377
|
+
* callers can keep using the state. {@link dispose} composes this with the
|
|
378
|
+
* close step so normal session shutdown promotes working memory to
|
|
379
|
+
* episodic/gists/graph automatically (see issue #2320).
|
|
380
|
+
*
|
|
381
|
+
* Aliased subagent states share `scoped` (and therefore the actual SQLite
|
|
382
|
+
* banks) with their parent. `consolidate()` deliberately does NOT
|
|
383
|
+
* short-circuit on `aliasOf`: `forceRetainCurrentSession` already guards
|
|
384
|
+
* itself, and an explicit `/memory enqueue` invoked from within a subagent
|
|
385
|
+
* still needs to flush extractions and sleep the parent's shared banks —
|
|
386
|
+
* otherwise enqueue would report success while leaving the subagent's
|
|
387
|
+
* retained memories unconsolidated until the parent eventually shuts down
|
|
388
|
+
* (PR #2327 review).
|
|
389
|
+
*/
|
|
390
|
+
async consolidate(): Promise<void> {
|
|
391
|
+
await this.forceRetainCurrentSession();
|
|
392
|
+
for (const memory of this.scoped.owned) {
|
|
393
|
+
await memory.flushExtractions();
|
|
394
|
+
memory.sleepAllSessions(false);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Release the per-session resources. Defaults to running {@link consolidate}
|
|
400
|
+
* before closing handles so normal session shutdown promotes working memory
|
|
401
|
+
* into long-term storage. Callers that are about to delete the DB files —
|
|
402
|
+
* e.g. `mnemopiBackend.clear` — pass `{ consolidate: false }` to skip the
|
|
403
|
+
* extraction/sleep pass, since spending tokens on memories that will be
|
|
404
|
+
* wiped on the next line is wasted work (PR #2327 review).
|
|
405
|
+
*/
|
|
406
|
+
async dispose(options: { consolidate?: boolean } = {}): Promise<void> {
|
|
374
407
|
this.unsubscribe?.();
|
|
375
408
|
this.unsubscribe = undefined;
|
|
376
|
-
if (
|
|
377
|
-
|
|
409
|
+
if (this.aliasOf) return;
|
|
410
|
+
if (options.consolidate !== false) {
|
|
411
|
+
try {
|
|
412
|
+
await this.consolidate();
|
|
413
|
+
} catch (error) {
|
|
414
|
+
logger.warn("Mnemopi: consolidation on dispose failed.", { error: String(error) });
|
|
415
|
+
}
|
|
378
416
|
}
|
|
417
|
+
for (const memory of this.scoped.owned) memory.close();
|
|
379
418
|
}
|
|
380
419
|
}
|
|
381
420
|
|
|
382
421
|
// `per-project-tagged` is implemented by opening both the project bank and the
|
|
383
422
|
// shared bank, then merging recall results while keeping writes project-local.
|
|
384
423
|
function createScopedResources(config: MnemopiBackendConfig): MnemopiScopedResources {
|
|
424
|
+
// Env vars (MNEMOPI_POLYPHONIC_RECALL / MNEMOPI_ENHANCED_RECALL) still override
|
|
425
|
+
// these config-driven defaults inside the core gates.
|
|
426
|
+
requireMnemopi().configureRecallFeatures({
|
|
427
|
+
polyphonicRecall: config.polyphonicRecall,
|
|
428
|
+
enhancedRecall: config.enhancedRecall,
|
|
429
|
+
});
|
|
385
430
|
const banks = resolveScopedBanks(config);
|
|
386
431
|
const memories = new Map<string, MnemopiScopedMemory>();
|
|
387
432
|
const open = (bank: string): MnemopiScopedMemory => {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Shows name, description, origin, status, and kind-specific preview.
|
|
5
5
|
*/
|
|
6
6
|
import * as os from "node:os";
|
|
7
|
+
import { isZodSchema, zodToWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
|
|
7
8
|
import { type Component, truncateToWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
|
|
8
9
|
import { theme } from "../../../modes/theme/theme";
|
|
9
10
|
import { shortenPath } from "../../../tools/render-utils";
|
|
@@ -168,12 +169,15 @@ export class InspectorPanel implements Component {
|
|
|
168
169
|
|
|
169
170
|
try {
|
|
170
171
|
const tool = raw as any;
|
|
171
|
-
const
|
|
172
|
+
const wire = (s: unknown): any => (isZodSchema(s) ? zodToWireSchema(s) : s);
|
|
173
|
+
const paramSchema = wire(tool?.parameters);
|
|
174
|
+
const inputSchema = wire(tool?.inputSchema);
|
|
175
|
+
const params = paramSchema?.properties || inputSchema?.properties || {};
|
|
172
176
|
|
|
173
177
|
if (Object.keys(params).length === 0) {
|
|
174
178
|
lines.push(theme.fg("dim", " (no arguments)"));
|
|
175
179
|
} else {
|
|
176
|
-
const required = new Set(
|
|
180
|
+
const required = new Set(paramSchema?.required || inputSchema?.required || []);
|
|
177
181
|
|
|
178
182
|
for (const [name, spec] of Object.entries(params)) {
|
|
179
183
|
const param = spec as any;
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
Markdown,
|
|
24
24
|
type MarkdownTheme,
|
|
25
25
|
matchesKey,
|
|
26
|
+
parseSgrMouse,
|
|
26
27
|
ScrollView,
|
|
27
28
|
truncateToWidth,
|
|
28
29
|
visibleWidth,
|
|
@@ -141,7 +142,7 @@ export class PlanReviewOverlay implements Component {
|
|
|
141
142
|
#optionClickRows = new Map<number, number>();
|
|
142
143
|
#tocClickRows = new Map<number, number>();
|
|
143
144
|
#bodyClickRows = new Set<number>();
|
|
144
|
-
/**
|
|
145
|
+
/** Exclusive 0-based column bound below which a region-row click targets the sidebar. */
|
|
145
146
|
#sidebarClickMaxCol = 0;
|
|
146
147
|
/** Option index the pointer is currently hovering, or undefined. Updated from
|
|
147
148
|
* motion mouse reports and cleared when the pointer leaves the option rows. */
|
|
@@ -332,26 +333,23 @@ export class PlanReviewOverlay implements Component {
|
|
|
332
333
|
* the body.
|
|
333
334
|
*/
|
|
334
335
|
#handleMouse(data: string): boolean {
|
|
335
|
-
const
|
|
336
|
-
if (!
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (button & 64) {
|
|
341
|
-
// Scroll wheel: low bit selects direction (64 up, 65 down).
|
|
342
|
-
this.#scrollView.scroll(button & 1 ? 3 : -3);
|
|
336
|
+
const event = parseSgrMouse(data);
|
|
337
|
+
if (!event) return false;
|
|
338
|
+
if (event.wheel !== null) {
|
|
339
|
+
// Scroll wheel: three rows per notch.
|
|
340
|
+
this.#scrollView.scroll(event.wheel * 3);
|
|
343
341
|
return true;
|
|
344
342
|
}
|
|
345
|
-
if (
|
|
346
|
-
if (
|
|
343
|
+
if (event.release) return true;
|
|
344
|
+
if (event.motion) {
|
|
347
345
|
// Motion (hover or drag): light up the option row under the pointer so a
|
|
348
346
|
// mouse user gets the same affordance the keyboard cursor gives. Any
|
|
349
347
|
// non-option row clears the highlight.
|
|
350
|
-
this.#setHoveredOption(this.#optionClickRows.get(row));
|
|
348
|
+
this.#setHoveredOption(this.#optionClickRows.get(event.row));
|
|
351
349
|
return true;
|
|
352
350
|
}
|
|
353
|
-
if (
|
|
354
|
-
const optionIndex = this.#optionClickRows.get(row);
|
|
351
|
+
if (!event.leftClick) return true;
|
|
352
|
+
const optionIndex = this.#optionClickRows.get(event.row);
|
|
355
353
|
if (optionIndex !== undefined) {
|
|
356
354
|
if (!this.#disabled.has(optionIndex)) {
|
|
357
355
|
this.#focus = "actions";
|
|
@@ -360,14 +358,14 @@ export class PlanReviewOverlay implements Component {
|
|
|
360
358
|
}
|
|
361
359
|
return true;
|
|
362
360
|
}
|
|
363
|
-
const tocPos = this.#tocClickRows.get(row);
|
|
364
|
-
if (tocPos !== undefined &&
|
|
361
|
+
const tocPos = this.#tocClickRows.get(event.row);
|
|
362
|
+
if (tocPos !== undefined && event.col < this.#sidebarClickMaxCol) {
|
|
365
363
|
this.#focus = "toc";
|
|
366
364
|
this.#tocCursor = tocPos;
|
|
367
365
|
this.#scrubBodyToToc();
|
|
368
366
|
return true;
|
|
369
367
|
}
|
|
370
|
-
if (this.#bodyClickRows.has(row)) {
|
|
368
|
+
if (this.#bodyClickRows.has(event.row)) {
|
|
371
369
|
this.#setFocus("body");
|
|
372
370
|
}
|
|
373
371
|
return true;
|
|
@@ -629,11 +629,18 @@ export class PluginSettingsComponent extends Container {
|
|
|
629
629
|
this.#currentMarketplacePlugin = null;
|
|
630
630
|
this.clear();
|
|
631
631
|
|
|
632
|
-
// Surface
|
|
633
|
-
//
|
|
634
|
-
//
|
|
632
|
+
// Surface registry failures without taking the whole tab down — either
|
|
633
|
+
// registry can fail to load (corrupt JSON, missing project root) and the
|
|
634
|
+
// user still benefits from the other half. An uncaught rejection here
|
|
635
|
+
// would also leave the tab permanently blank: this method is invoked
|
|
636
|
+
// fire-and-forget from the constructor, so nothing awaits it.
|
|
635
637
|
const [npmPlugins, marketplacePlugins] = await Promise.all([
|
|
636
|
-
this.#manager.list()
|
|
638
|
+
this.#manager.list().catch(err => {
|
|
639
|
+
logger.error("Settings → Plugins: failed to list npm plugins", {
|
|
640
|
+
error: err instanceof Error ? err.message : String(err),
|
|
641
|
+
});
|
|
642
|
+
return [] as InstalledPlugin[];
|
|
643
|
+
}),
|
|
637
644
|
this.#buildMarketplaceManager()
|
|
638
645
|
.then(mgr => mgr.listInstalledPlugins())
|
|
639
646
|
.catch(err => {
|
|
@@ -717,6 +724,16 @@ export class PluginSettingsComponent extends Container {
|
|
|
717
724
|
}
|
|
718
725
|
|
|
719
726
|
handleInput(data: string): void {
|
|
720
|
-
this.#viewComponent
|
|
727
|
+
if (!this.#viewComponent) {
|
|
728
|
+
// The list view mounts asynchronously (npm + marketplace listing).
|
|
729
|
+
// Until it does — or if listing rejected and no view ever mounted —
|
|
730
|
+
// Escape must still close the panel instead of leaving /settings
|
|
731
|
+
// non-dismissible.
|
|
732
|
+
if (data === "\x1b" || data === "\x1b\x1b") {
|
|
733
|
+
this.callbacks.onClose();
|
|
734
|
+
}
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
this.#viewComponent.handleInput(data);
|
|
721
738
|
}
|
|
722
739
|
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* settings selector.
|
|
5
5
|
*
|
|
6
6
|
* To add a new setting to the UI: declare it in `settings-schema.ts`
|
|
7
|
-
* with a `ui` block
|
|
7
|
+
* with a `ui` block carrying `tab` and `group` (the group must be listed
|
|
8
|
+
* in `TAB_GROUPS[tab]`). If it needs a submenu, include `options: [...]`
|
|
8
9
|
* (or `options: "runtime"` for runtime-injected lists like themes).
|
|
9
10
|
*/
|
|
10
11
|
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
type SettingPath,
|
|
22
23
|
type SettingTab,
|
|
23
24
|
type SubmenuOption,
|
|
25
|
+
TAB_GROUPS,
|
|
24
26
|
} from "../../config/settings-schema";
|
|
25
27
|
|
|
26
28
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -34,6 +36,8 @@ interface BaseSettingDef {
|
|
|
34
36
|
label: string;
|
|
35
37
|
description: string;
|
|
36
38
|
tab: SettingTab;
|
|
39
|
+
/** Section within the tab; items are ordered by TAB_GROUPS[tab] and rendered under a heading row. */
|
|
40
|
+
group?: string;
|
|
37
41
|
/**
|
|
38
42
|
* Optional visibility predicate. When supplied and returning false, the
|
|
39
43
|
* setting is hidden from the UI. Applies to every variant — booleans,
|
|
@@ -111,7 +115,7 @@ function pathToSettingDef(path: SettingPath): SettingDef | null {
|
|
|
111
115
|
|
|
112
116
|
const schemaType = getType(path);
|
|
113
117
|
const condition = ui.condition ? CONDITIONS[ui.condition] : undefined;
|
|
114
|
-
const base = { path, label: ui.label, description: ui.description, tab: ui.tab, condition };
|
|
118
|
+
const base = { path, label: ui.label, description: ui.description, tab: ui.tab, group: ui.group, condition };
|
|
115
119
|
|
|
116
120
|
if (schemaType === "boolean") {
|
|
117
121
|
return { ...base, type: "boolean" };
|
|
@@ -170,9 +174,20 @@ export function getAllSettingDefs(): SettingDef[] {
|
|
|
170
174
|
return defs;
|
|
171
175
|
}
|
|
172
176
|
|
|
173
|
-
/**
|
|
177
|
+
/**
|
|
178
|
+
* Get settings for a specific tab, ordered by the tab's group layout
|
|
179
|
+
* (TAB_GROUPS). Ungrouped settings sort first; within a group, schema
|
|
180
|
+
* declaration order is preserved.
|
|
181
|
+
*/
|
|
174
182
|
export function getSettingsForTab(tab: SettingTab): SettingDef[] {
|
|
175
|
-
|
|
183
|
+
const defs = getAllSettingDefs().filter(def => def.tab === tab);
|
|
184
|
+
const order = TAB_GROUPS[tab];
|
|
185
|
+
const rank = (def: SettingDef): number => {
|
|
186
|
+
if (!def.group) return -1;
|
|
187
|
+
const index = order.indexOf(def.group);
|
|
188
|
+
return index >= 0 ? index : order.length;
|
|
189
|
+
};
|
|
190
|
+
return defs.sort((a, b) => rank(a) - rank(b));
|
|
176
191
|
}
|
|
177
192
|
|
|
178
193
|
/** Get a setting definition by path */
|