@gavin-hu/my-pi-agent 0.0.0-stage → 0.7.0
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/LICENSE +21 -0
- package/README.md +160 -2
- package/assets/bytefly-chrome.png +0 -0
- package/assets/bytefly.schema.json +769 -0
- package/assets/file-browse.png +0 -0
- package/assets/plan-review.png +0 -0
- package/extensions/bytefly/README.md +820 -0
- package/extensions/bytefly/doctor/commands.ts +68 -0
- package/extensions/bytefly/doctor/config.ts +47 -0
- package/extensions/bytefly/doctor/format.ts +188 -0
- package/extensions/bytefly/doctor/register.ts +54 -0
- package/extensions/bytefly/doctor/runtime.ts +87 -0
- package/extensions/bytefly/doctor/schema.ts +51 -0
- package/extensions/bytefly/doctor/tools.ts +113 -0
- package/extensions/bytefly/doctor/tui.ts +140 -0
- package/extensions/bytefly/footer/collect/entries.ts +137 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +64 -0
- package/extensions/bytefly/footer/config.ts +35 -0
- package/extensions/bytefly/footer/price/config.ts +152 -0
- package/extensions/bytefly/footer/price/price.ts +59 -0
- package/extensions/bytefly/footer/price/rates.ts +54 -0
- package/extensions/bytefly/footer/price/schedule.ts +112 -0
- package/extensions/bytefly/footer/price/types.ts +53 -0
- package/extensions/bytefly/footer/register.ts +116 -0
- package/extensions/bytefly/footer/render/footer.ts +51 -0
- package/extensions/bytefly/footer/render/format.ts +106 -0
- package/extensions/bytefly/footer/render/layout.ts +139 -0
- package/extensions/bytefly/footer/render/lines.ts +50 -0
- package/extensions/bytefly/footer/render/segments.ts +281 -0
- package/extensions/bytefly/footer/render/status.ts +42 -0
- package/extensions/bytefly/footer/types.ts +104 -0
- package/extensions/bytefly/header/component.ts +112 -0
- package/extensions/bytefly/header/config.ts +77 -0
- package/extensions/bytefly/header/register.ts +53 -0
- package/extensions/bytefly/index.ts +48 -0
- package/extensions/bytefly/notify/config.ts +60 -0
- package/extensions/bytefly/notify/dispatch.ts +138 -0
- package/extensions/bytefly/notify/format.ts +39 -0
- package/extensions/bytefly/notify/register.ts +85 -0
- package/extensions/bytefly/notify/types.ts +28 -0
- package/extensions/bytefly/picker/commands.ts +73 -0
- package/extensions/bytefly/picker/discovery.ts +128 -0
- package/extensions/bytefly/picker/resources.ts +226 -0
- package/extensions/bytefly/picker/runtime.ts +108 -0
- package/extensions/bytefly/picker/toggle.ts +145 -0
- package/extensions/bytefly/picker/tui.ts +260 -0
- package/extensions/bytefly/picker/types.ts +55 -0
- package/extensions/bytefly/separator/component.ts +26 -0
- package/extensions/bytefly/separator/config.ts +24 -0
- package/extensions/bytefly/separator/format.ts +50 -0
- package/extensions/bytefly/separator/register.ts +42 -0
- package/extensions/bytefly/separator/types.ts +22 -0
- package/extensions/document/README.md +281 -0
- package/extensions/document/config.ts +58 -0
- package/extensions/document/extract/doc.ts +19 -0
- package/extensions/document/extract/docx.ts +18 -0
- package/extensions/document/extract/file.ts +45 -0
- package/extensions/document/extract/odt.ts +101 -0
- package/extensions/document/extract/pdf.ts +25 -0
- package/extensions/document/extract/rtf.ts +45 -0
- package/extensions/document/extract/vendor.d.ts +39 -0
- package/extensions/document/extract/xlsx.ts +88 -0
- package/extensions/document/formats.ts +59 -0
- package/extensions/document/index.ts +17 -0
- package/extensions/document/paging.ts +110 -0
- package/extensions/document/render.ts +105 -0
- package/extensions/document/schema.ts +50 -0
- package/extensions/document/tool.ts +152 -0
- package/extensions/file-browse/README.md +269 -0
- package/extensions/file-browse/client.ts +550 -0
- package/extensions/file-browse/config.ts +82 -0
- package/extensions/file-browse/files.ts +433 -0
- package/extensions/file-browse/fuzzy.ts +75 -0
- package/extensions/file-browse/git.ts +74 -0
- package/extensions/file-browse/html.ts +346 -0
- package/extensions/file-browse/icons.ts +119 -0
- package/extensions/file-browse/index.ts +149 -0
- package/extensions/file-browse/markdown.ts +592 -0
- package/extensions/file-browse/paths.ts +101 -0
- package/extensions/file-browse/router.ts +289 -0
- package/extensions/file-browse/server.ts +104 -0
- package/extensions/file-open/README.md +135 -0
- package/extensions/file-open/index.ts +93 -0
- package/extensions/file-open/open.ts +102 -0
- package/extensions/file-open/schema.ts +31 -0
- package/extensions/goal/README.md +241 -0
- package/extensions/goal/clarify-parse.ts +203 -0
- package/extensions/goal/clarify-prompt.ts +80 -0
- package/extensions/goal/clarify.ts +234 -0
- package/extensions/goal/commands.ts +220 -0
- package/extensions/goal/config.ts +35 -0
- package/extensions/goal/format.ts +111 -0
- package/extensions/goal/index.ts +77 -0
- package/extensions/goal/runtime.ts +90 -0
- package/extensions/goal/schema.ts +105 -0
- package/extensions/goal/state.ts +69 -0
- package/extensions/goal/tools.ts +136 -0
- package/extensions/goal/tui.ts +97 -0
- package/extensions/goal/types.ts +42 -0
- package/extensions/job/README.md +309 -0
- package/extensions/job/commands.ts +74 -0
- package/extensions/job/config.ts +68 -0
- package/extensions/job/format.ts +256 -0
- package/extensions/job/index.ts +140 -0
- package/extensions/job/logs.ts +56 -0
- package/extensions/job/paths.ts +31 -0
- package/extensions/job/process.ts +106 -0
- package/extensions/job/registry.ts +265 -0
- package/extensions/job/reservations.ts +82 -0
- package/extensions/job/runtime.ts +846 -0
- package/extensions/job/schema.ts +148 -0
- package/extensions/job/session.ts +116 -0
- package/extensions/job/status.ts +69 -0
- package/extensions/job/store.ts +103 -0
- package/extensions/job/tools.ts +295 -0
- package/extensions/job/tui.ts +486 -0
- package/extensions/job/types.ts +92 -0
- package/extensions/job/ui.ts +99 -0
- package/extensions/job/waiters.ts +46 -0
- package/extensions/memory/README.md +221 -0
- package/extensions/memory/commands.ts +190 -0
- package/extensions/memory/config.ts +35 -0
- package/extensions/memory/errors.ts +9 -0
- package/extensions/memory/format.ts +141 -0
- package/extensions/memory/index.ts +59 -0
- package/extensions/memory/runtime.ts +199 -0
- package/extensions/memory/schema.ts +86 -0
- package/extensions/memory/store.ts +231 -0
- package/extensions/memory/tools.ts +165 -0
- package/extensions/memory/tui.ts +430 -0
- package/extensions/memory/types.ts +38 -0
- package/extensions/plan/README.md +417 -0
- package/extensions/plan/blocks.ts +197 -0
- package/extensions/plan/commands.ts +134 -0
- package/extensions/plan/feedback.ts +157 -0
- package/extensions/plan/git-bash.ts +320 -0
- package/extensions/plan/index.ts +179 -0
- package/extensions/plan/path-guard.ts +101 -0
- package/extensions/plan/plans.ts +267 -0
- package/extensions/plan/policy.ts +73 -0
- package/extensions/plan/runtime.ts +195 -0
- package/extensions/plan/steps.ts +228 -0
- package/extensions/plan/tools.ts +605 -0
- package/extensions/plan/tui.ts +391 -0
- package/extensions/plan/types.ts +67 -0
- package/extensions/plan/web-client.ts +433 -0
- package/extensions/plan/web-page.ts +169 -0
- package/extensions/plan/web-router.ts +155 -0
- package/extensions/plan/web.ts +267 -0
- package/extensions/question/README.md +189 -0
- package/extensions/question/answers.ts +119 -0
- package/extensions/question/dialogs.ts +109 -0
- package/extensions/question/index.ts +28 -0
- package/extensions/question/schema.ts +119 -0
- package/extensions/question/tools.ts +122 -0
- package/extensions/question/tui-state.ts +240 -0
- package/extensions/question/tui.ts +324 -0
- package/extensions/question/types.ts +61 -0
- package/extensions/rewind/README.md +202 -0
- package/extensions/rewind/commands.ts +74 -0
- package/extensions/rewind/config.ts +71 -0
- package/extensions/rewind/format.ts +110 -0
- package/extensions/rewind/git.ts +177 -0
- package/extensions/rewind/index.ts +60 -0
- package/extensions/rewind/policy.ts +42 -0
- package/extensions/rewind/queue.ts +27 -0
- package/extensions/rewind/restore.ts +96 -0
- package/extensions/rewind/rewind-tui.ts +135 -0
- package/extensions/rewind/rewind.ts +146 -0
- package/extensions/rewind/runtime.ts +174 -0
- package/extensions/rewind/snapshot-run.ts +103 -0
- package/extensions/rewind/snapshot.ts +80 -0
- package/extensions/rewind/status.ts +67 -0
- package/extensions/rewind/store.ts +142 -0
- package/extensions/rewind/temp-index.ts +87 -0
- package/extensions/rewind/timeline.ts +119 -0
- package/extensions/rewind/types.ts +58 -0
- package/extensions/subagent/README.md +310 -0
- package/extensions/subagent/agents.ts +454 -0
- package/extensions/subagent/format.ts +126 -0
- package/extensions/subagent/index.ts +181 -0
- package/extensions/subagent/invocation.ts +57 -0
- package/extensions/subagent/orchestrate.ts +182 -0
- package/extensions/subagent/render.ts +491 -0
- package/extensions/subagent/run.ts +212 -0
- package/extensions/subagent/schema.ts +102 -0
- package/extensions/subagent/stream.ts +144 -0
- package/extensions/subagent/types.ts +79 -0
- package/extensions/todo/README.md +230 -0
- package/extensions/todo/commands.ts +41 -0
- package/extensions/todo/config.ts +29 -0
- package/extensions/todo/format.ts +142 -0
- package/extensions/todo/index.ts +100 -0
- package/extensions/todo/nudge.ts +64 -0
- package/extensions/todo/runtime.ts +149 -0
- package/extensions/todo/schema.ts +125 -0
- package/extensions/todo/state.ts +65 -0
- package/extensions/todo/tools.ts +136 -0
- package/extensions/todo/tui.ts +170 -0
- package/extensions/todo/types.ts +33 -0
- package/extensions/watch/README.md +214 -0
- package/extensions/watch/commands.ts +144 -0
- package/extensions/watch/config.ts +59 -0
- package/extensions/watch/format.ts +212 -0
- package/extensions/watch/index.ts +129 -0
- package/extensions/watch/process.ts +63 -0
- package/extensions/watch/runtime.ts +525 -0
- package/extensions/watch/schema.ts +177 -0
- package/extensions/watch/tools.ts +278 -0
- package/extensions/watch/tui.ts +427 -0
- package/extensions/watch/types.ts +83 -0
- package/extensions/watch/ui.ts +97 -0
- package/extensions/web-access/README.md +347 -0
- package/extensions/web-access/config.ts +36 -0
- package/extensions/web-access/fetch/cache.ts +78 -0
- package/extensions/web-access/fetch/config.ts +81 -0
- package/extensions/web-access/fetch/extract.ts +124 -0
- package/extensions/web-access/fetch/find.ts +158 -0
- package/extensions/web-access/fetch/format.ts +117 -0
- package/extensions/web-access/fetch/page.ts +169 -0
- package/extensions/web-access/fetch/pdf.ts +45 -0
- package/extensions/web-access/fetch/schema.ts +192 -0
- package/extensions/web-access/fetch/ssrf.ts +191 -0
- package/extensions/web-access/fetch/tool.ts +238 -0
- package/extensions/web-access/fetch/types.ts +26 -0
- package/extensions/web-access/html.ts +41 -0
- package/extensions/web-access/http.ts +133 -0
- package/extensions/web-access/index.ts +28 -0
- package/extensions/web-access/search/config.ts +115 -0
- package/extensions/web-access/search/format.ts +65 -0
- package/extensions/web-access/search/json.ts +75 -0
- package/extensions/web-access/search/provider.ts +38 -0
- package/extensions/web-access/search/providers/brave.ts +76 -0
- package/extensions/web-access/search/providers/duckduckgo.ts +109 -0
- package/extensions/web-access/search/providers/searxng.ts +98 -0
- package/extensions/web-access/search/registry.ts +49 -0
- package/extensions/web-access/search/schema.ts +65 -0
- package/extensions/web-access/search/search.ts +129 -0
- package/extensions/web-access/search/tool.ts +112 -0
- package/extensions/web-access/search/types.ts +32 -0
- package/extensions/web-access/text.ts +26 -0
- package/extensions/web-access/transcript.ts +20 -0
- package/extensions/wechat/README.md +273 -0
- package/extensions/wechat/bridge.ts +577 -0
- package/extensions/wechat/client.ts +411 -0
- package/extensions/wechat/commands.ts +220 -0
- package/extensions/wechat/config.ts +88 -0
- package/extensions/wechat/credentials.ts +89 -0
- package/extensions/wechat/format.ts +76 -0
- package/extensions/wechat/index.ts +175 -0
- package/extensions/wechat/interaction.ts +130 -0
- package/extensions/wechat/lock.ts +86 -0
- package/extensions/wechat/login-screen.ts +91 -0
- package/extensions/wechat/login.ts +130 -0
- package/extensions/wechat/media.ts +103 -0
- package/extensions/wechat/outbound.ts +102 -0
- package/extensions/wechat/prompt.ts +104 -0
- package/extensions/wechat/qrcode.ts +46 -0
- package/extensions/wechat/remote-ui.ts +97 -0
- package/extensions/wechat/slash.ts +114 -0
- package/extensions/wechat/state.ts +71 -0
- package/extensions/wechat/status-screen.ts +176 -0
- package/extensions/wechat/tools.ts +109 -0
- package/extensions/wechat/types.ts +170 -0
- package/extensions/worktree/README.md +278 -0
- package/extensions/worktree/commands.ts +110 -0
- package/extensions/worktree/config.ts +107 -0
- package/extensions/worktree/enter.ts +273 -0
- package/extensions/worktree/errors.ts +18 -0
- package/extensions/worktree/exit.ts +122 -0
- package/extensions/worktree/git.ts +429 -0
- package/extensions/worktree/guard.ts +566 -0
- package/extensions/worktree/include.ts +147 -0
- package/extensions/worktree/index.ts +221 -0
- package/extensions/worktree/lifecycle.ts +14 -0
- package/extensions/worktree/prune.ts +99 -0
- package/extensions/worktree/registry.ts +221 -0
- package/extensions/worktree/root-tools.ts +162 -0
- package/extensions/worktree/runtime.ts +135 -0
- package/extensions/worktree/state.ts +65 -0
- package/extensions/worktree/status.ts +115 -0
- package/extensions/worktree/tools.ts +147 -0
- package/lib/README.md +49 -0
- package/lib/browser.ts +40 -0
- package/lib/config.ts +186 -0
- package/lib/env.ts +41 -0
- package/lib/escape.ts +27 -0
- package/lib/events.ts +40 -0
- package/lib/format.ts +76 -0
- package/lib/git/index.ts +7 -0
- package/lib/git/read.ts +76 -0
- package/lib/git/runner.ts +80 -0
- package/lib/highlight.ts +1000 -0
- package/lib/interaction.ts +132 -0
- package/lib/list-cursor.ts +222 -0
- package/lib/path.ts +49 -0
- package/lib/pi-invocation.ts +71 -0
- package/lib/policy.ts +67 -0
- package/lib/rails.ts +91 -0
- package/lib/requirements.ts +431 -0
- package/lib/shell.ts +33 -0
- package/lib/tool-names.ts +32 -0
- package/lib/tui.ts +184 -0
- package/lib/ui.ts +105 -0
- package/lib/web/base.ts +18 -0
- package/lib/web/components.ts +122 -0
- package/lib/web/index.ts +12 -0
- package/lib/web/layout.ts +30 -0
- package/lib/web/markup.ts +99 -0
- package/lib/web/runtime.ts +68 -0
- package/lib/web/tokens.ts +128 -0
- package/package.json +134 -4
- package/themes/bytefly-dark.json +91 -0
- package/themes/bytefly-light.json +91 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing registration for the `memory` tool.
|
|
3
|
+
*
|
|
4
|
+
* One singleton tool with an action switch, like `job`: `add`, `update`, and
|
|
5
|
+
* `forget` mutate one scope, `list` reads both. Validation failures come back as
|
|
6
|
+
* an error result carrying the unchanged notes, so the model can retry without
|
|
7
|
+
* corrupting a store. An `add` that duplicates an existing note is a no-op that
|
|
8
|
+
* names the stored note.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
12
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { sanitizeLine } from "../../lib/format.ts";
|
|
14
|
+
import { reuseText, toolDetails } from "../../lib/tui.ts";
|
|
15
|
+
import { messageOf } from "./errors.ts";
|
|
16
|
+
import { formatCallText, formatResultText, scopeLabel } from "./format.ts";
|
|
17
|
+
import type { MemoryRuntime } from "./runtime.ts";
|
|
18
|
+
import {
|
|
19
|
+
MemoryParams,
|
|
20
|
+
MemoryResult,
|
|
21
|
+
normalizeAction,
|
|
22
|
+
normalizeEntryText,
|
|
23
|
+
normalizeScope,
|
|
24
|
+
type MemoryArgs,
|
|
25
|
+
} from "./schema.ts";
|
|
26
|
+
import type { MemoryDetails, MemoryScope } from "./types.ts";
|
|
27
|
+
|
|
28
|
+
export const TOOL_NAME = "memory";
|
|
29
|
+
|
|
30
|
+
/** JSON-safe mirror of the details for `structuredContent`. */
|
|
31
|
+
function toStructuredContent(details: MemoryDetails): Record<string, JsonValue> {
|
|
32
|
+
const content: Record<string, JsonValue> = {
|
|
33
|
+
action: details.action,
|
|
34
|
+
scope: details.scope,
|
|
35
|
+
project: details.project,
|
|
36
|
+
global: details.global,
|
|
37
|
+
changed: details.changed,
|
|
38
|
+
};
|
|
39
|
+
if (details.entry !== undefined) content.entry = details.entry;
|
|
40
|
+
if (details.to !== undefined) content.to = details.to;
|
|
41
|
+
if (details.existing !== undefined) content.existing = details.existing;
|
|
42
|
+
if (details.error !== undefined) content.error = details.error;
|
|
43
|
+
return content;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build the tool result for one call. A rejected call carries the unchanged
|
|
48
|
+
* notes and is marked `isError`; nothing was written.
|
|
49
|
+
*/
|
|
50
|
+
function memoryResult(details: MemoryDetails, error?: string) {
|
|
51
|
+
const full: MemoryDetails = error === undefined ? details : { ...details, error };
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: "text" as const, text: formatResultText(full) }],
|
|
54
|
+
details: full,
|
|
55
|
+
structuredContent: toStructuredContent(full),
|
|
56
|
+
...(error === undefined ? {} : { isError: true as const }),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function registerTools(pi: ExtensionAPI, runtime: MemoryRuntime): void {
|
|
61
|
+
pi.registerTool({
|
|
62
|
+
name: TOOL_NAME,
|
|
63
|
+
label: "Memory",
|
|
64
|
+
description: [
|
|
65
|
+
"Save, change, remove, or list short durable notes that are recalled automatically in later sessions.",
|
|
66
|
+
'Notes are scoped "project" (the current repository, the default) or "global" (every project); stored notes',
|
|
67
|
+
"are injected into context before each run. Adding a note that already exists, ignoring case and trailing",
|
|
68
|
+
"punctuation, is a no-op.",
|
|
69
|
+
].join(" "),
|
|
70
|
+
promptSnippet: "Save, change, or remove a durable note that is recalled in later sessions.",
|
|
71
|
+
promptGuidelines: [
|
|
72
|
+
"Use memory for short, durable facts the user asks you to remember; keep one fact per call.",
|
|
73
|
+
"Use action list only to see what is stored; the notes are already in context.",
|
|
74
|
+
"To remove a note, pass its exact text with action forget; bulk clearing is not available.",
|
|
75
|
+
"To change a note, use action update with its exact text and the replacement in to; do not forget and re-add.",
|
|
76
|
+
],
|
|
77
|
+
parameters: MemoryParams,
|
|
78
|
+
outputSchema: MemoryResult,
|
|
79
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
80
|
+
executionMode: "sequential",
|
|
81
|
+
|
|
82
|
+
async execute(_toolCallId, params, _signal, _onUpdate) {
|
|
83
|
+
const args = params as MemoryArgs;
|
|
84
|
+
let action: MemoryDetails["action"];
|
|
85
|
+
let scope: MemoryScope;
|
|
86
|
+
try {
|
|
87
|
+
action = normalizeAction(args.action);
|
|
88
|
+
scope = normalizeScope(args.scope);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return memoryResult(
|
|
91
|
+
{ action: "list", scope: "project", ...runtime.entries(), changed: false },
|
|
92
|
+
messageOf(error),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
if (action === "list") {
|
|
98
|
+
return memoryResult({ action, scope, ...runtime.entries(), changed: false });
|
|
99
|
+
}
|
|
100
|
+
const text = normalizeEntryText(args.text);
|
|
101
|
+
if (action === "add") {
|
|
102
|
+
const result = await runtime.add(scope, text);
|
|
103
|
+
return memoryResult({
|
|
104
|
+
action,
|
|
105
|
+
scope,
|
|
106
|
+
...runtime.entries(),
|
|
107
|
+
changed: result.changed,
|
|
108
|
+
entry: text,
|
|
109
|
+
...(result.existing === undefined ? {} : { existing: result.existing }),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (action === "update") {
|
|
113
|
+
const to = normalizeEntryText(args.to, "to");
|
|
114
|
+
if (text === to) {
|
|
115
|
+
return memoryResult({ action, scope, ...runtime.entries(), changed: false, entry: text, to });
|
|
116
|
+
}
|
|
117
|
+
const changed = await runtime.update(scope, text, to);
|
|
118
|
+
if (!changed) {
|
|
119
|
+
return memoryResult(
|
|
120
|
+
{ action, scope, ...runtime.entries(), changed: false, entry: text, to },
|
|
121
|
+
`No ${scopeLabel(scope)} memory note matches: ${text}`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return memoryResult({ action, scope, ...runtime.entries(), changed, entry: text, to });
|
|
125
|
+
}
|
|
126
|
+
const changed = await runtime.forget(scope, text);
|
|
127
|
+
if (!changed) {
|
|
128
|
+
return memoryResult(
|
|
129
|
+
{ action, scope, ...runtime.entries(), changed: false, entry: text },
|
|
130
|
+
`No ${scopeLabel(scope)} memory note matches: ${text}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return memoryResult({ action, scope, ...runtime.entries(), changed, entry: text });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return memoryResult({ action, scope, ...runtime.entries(), changed: false }, messageOf(error));
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
renderCall(args, theme, context) {
|
|
140
|
+
const text = reuseText(context.lastComponent);
|
|
141
|
+
const body = formatCallText(
|
|
142
|
+
args as { action?: unknown; text?: unknown; to?: unknown; scope?: unknown },
|
|
143
|
+
context.argsComplete,
|
|
144
|
+
);
|
|
145
|
+
text.setText(theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + theme.fg("muted", body));
|
|
146
|
+
return text;
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
renderResult(result, { expanded }, theme, context) {
|
|
150
|
+
const text = reuseText(context.lastComponent);
|
|
151
|
+
const details = toolDetails<MemoryDetails>(result.details);
|
|
152
|
+
if (!details) {
|
|
153
|
+
const first = result.content[0];
|
|
154
|
+
text.setText(first?.type === "text" ? first.text : "");
|
|
155
|
+
return text;
|
|
156
|
+
}
|
|
157
|
+
if (details.error) {
|
|
158
|
+
text.setText(theme.fg("error", `Error: ${sanitizeLine(details.error)}`));
|
|
159
|
+
return text;
|
|
160
|
+
}
|
|
161
|
+
text.setText(formatResultText(details, expanded));
|
|
162
|
+
return text;
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal UI for the memory extension: the `/memory` picker.
|
|
3
|
+
*
|
|
4
|
+
* `MemoryPickerComponent` is a derived dock screen. It reads the live notes on
|
|
5
|
+
* every render (`entries()` refreshes from disk first) and asks for a repaint
|
|
6
|
+
* after an action settles, so an external edit or a mutation is reflected
|
|
7
|
+
* without the component owning any authoritative state. Navigation and scroll
|
|
8
|
+
* math come from `lib/list-cursor.ts` and the screen chrome from `lib/tui.ts`;
|
|
9
|
+
* the inline note editor is the shared `Editor` from `@earendil-works/pi-tui`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import {
|
|
14
|
+
Editor,
|
|
15
|
+
Key,
|
|
16
|
+
matchesKey,
|
|
17
|
+
truncateToWidth,
|
|
18
|
+
type Component,
|
|
19
|
+
type TUI,
|
|
20
|
+
type TuiMouseEvent,
|
|
21
|
+
type TuiMouseEventResult,
|
|
22
|
+
} from "@earendil-works/pi-tui";
|
|
23
|
+
import { sanitizeLine } from "../../lib/format.ts";
|
|
24
|
+
import {
|
|
25
|
+
fitRows,
|
|
26
|
+
formatRange,
|
|
27
|
+
isCloseKey,
|
|
28
|
+
ListCursor,
|
|
29
|
+
navigateList,
|
|
30
|
+
scrollList,
|
|
31
|
+
selectionMarker,
|
|
32
|
+
} from "../../lib/list-cursor.ts";
|
|
33
|
+
import { dockListFrame, editorTheme, screenHint, type ViewportRowsSource } from "../../lib/tui.ts";
|
|
34
|
+
import { messageOf } from "./errors.ts";
|
|
35
|
+
import { scopeLabel, type MemoryEntries } from "./format.ts";
|
|
36
|
+
import type { MemoryScope } from "./types.ts";
|
|
37
|
+
|
|
38
|
+
/** Note limit enforced by the `memory` tool; the picker mirrors it so a save is never rejected late. */
|
|
39
|
+
const MAX_NOTE_CHARS = 500;
|
|
40
|
+
|
|
41
|
+
/** Rows the screen shows when the terminal height is unknown. */
|
|
42
|
+
const SCREEN_DEFAULT_ROWS = 14;
|
|
43
|
+
|
|
44
|
+
/** Fixed rows around the list: header, summary, blank, blank, hint, blank. */
|
|
45
|
+
const CHROME = 6;
|
|
46
|
+
|
|
47
|
+
/** Which scopes the list shows. */
|
|
48
|
+
export type MemoryFilter = "all" | MemoryScope;
|
|
49
|
+
|
|
50
|
+
/** The live store and the mutations the picker drives. */
|
|
51
|
+
export interface MemoryPickerCallbacks {
|
|
52
|
+
/** Current notes, refreshed from disk first. */
|
|
53
|
+
entries(): MemoryEntries;
|
|
54
|
+
/** Whether the project store may be read and written. */
|
|
55
|
+
trusted(): boolean;
|
|
56
|
+
/** Re-read any changed store; returns whether either changed. */
|
|
57
|
+
refresh(): boolean;
|
|
58
|
+
add(scope: MemoryScope, text: string): Promise<{ changed: boolean; existing?: string }>;
|
|
59
|
+
update(scope: MemoryScope, from: string, to: string): Promise<boolean>;
|
|
60
|
+
forget(scope: MemoryScope, entry: string): Promise<boolean>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** One note in the flat, filtered list. */
|
|
64
|
+
interface Note {
|
|
65
|
+
scope: MemoryScope;
|
|
66
|
+
text: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Identity of a note: its scope and exact text (unique within a scope). */
|
|
70
|
+
function noteKey(note: Note): string {
|
|
71
|
+
return `${note.scope}\u0000${note.text}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Dismissible `/memory` screen: a scope-filtered note list with inline actions. */
|
|
75
|
+
export class MemoryPickerComponent implements Component {
|
|
76
|
+
private readonly cursor: ListCursor;
|
|
77
|
+
private readonly editor: Editor;
|
|
78
|
+
|
|
79
|
+
private filter: MemoryFilter = "all";
|
|
80
|
+
private mode: "list" | "edit" | "add" = "list";
|
|
81
|
+
/** Scope the inline editor writes to. */
|
|
82
|
+
private editorScope: MemoryScope = "project";
|
|
83
|
+
/** Note being edited, or undefined while adding. */
|
|
84
|
+
private editing: Note | undefined;
|
|
85
|
+
/** Note awaiting a y/N forget confirmation. */
|
|
86
|
+
private confirm: Note | undefined;
|
|
87
|
+
/** Transient feedback shown in place of the hint. */
|
|
88
|
+
private notice: string | undefined;
|
|
89
|
+
private componentFocused = false;
|
|
90
|
+
/** Stable selection identity, re-derived when the filtered list changes. */
|
|
91
|
+
private selectedKey: string | undefined;
|
|
92
|
+
/** Serialises mutations so a rapid double press cannot interleave writes. */
|
|
93
|
+
private chain: Promise<void> = Promise.resolve();
|
|
94
|
+
|
|
95
|
+
constructor(
|
|
96
|
+
tui: TUI,
|
|
97
|
+
private readonly theme: Theme,
|
|
98
|
+
private readonly callbacks: MemoryPickerCallbacks,
|
|
99
|
+
private readonly onClose: () => void,
|
|
100
|
+
private readonly requestRender: () => void,
|
|
101
|
+
private readonly viewportRowsSource?: ViewportRowsSource,
|
|
102
|
+
) {
|
|
103
|
+
this.cursor = new ListCursor(() => this.requestRender());
|
|
104
|
+
this.editor = new Editor(tui, editorTheme(theme));
|
|
105
|
+
this.editor.onSubmit = (value) => this.submitEditor(value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The filter, downgraded to `global` when the project is untrusted. */
|
|
109
|
+
private effectiveFilter(): MemoryFilter {
|
|
110
|
+
return this.filter === "project" && !this.callbacks.trusted() ? "global" : this.filter;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The flat, filtered note list, in project-then-global order. */
|
|
114
|
+
private notes(): Note[] {
|
|
115
|
+
const entries = this.callbacks.entries();
|
|
116
|
+
const filter = this.effectiveFilter();
|
|
117
|
+
const scopes: MemoryScope[] =
|
|
118
|
+
filter === "all" ? (this.callbacks.trusted() ? ["project", "global"] : ["global"]) : [filter];
|
|
119
|
+
const notes: Note[] = [];
|
|
120
|
+
for (const scope of scopes) {
|
|
121
|
+
for (const text of entries[scope]) notes.push({ scope, text });
|
|
122
|
+
}
|
|
123
|
+
return notes;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The scope an action targets: the filter's scope, or the best default. */
|
|
127
|
+
private activeScope(): MemoryScope {
|
|
128
|
+
if (this.filter === "global") return "global";
|
|
129
|
+
if (this.filter === "project" && this.callbacks.trusted()) return "project";
|
|
130
|
+
return this.callbacks.trusted() ? "project" : "global";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Index of the selected note in `notes`, re-derived from its key. */
|
|
134
|
+
private focusIndex(notes: Note[]): number {
|
|
135
|
+
if (notes.length === 0) return 0;
|
|
136
|
+
const found = this.selectedKey ? notes.findIndex((note) => noteKey(note) === this.selectedKey) : -1;
|
|
137
|
+
return found >= 0 ? found : 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Move the focus to `index`, recording the note's key. Never repaints. */
|
|
141
|
+
private focusIndexNow(index: number, notes: Note[]): void {
|
|
142
|
+
if (notes.length === 0) {
|
|
143
|
+
this.selectedKey = undefined;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const clamped = Math.min(Math.max(0, index), notes.length - 1);
|
|
147
|
+
this.selectedKey = noteKey(notes[clamped]);
|
|
148
|
+
this.cursor.selected = clamped;
|
|
149
|
+
this.cursor.sync(notes.length);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private cycleFilter(): void {
|
|
153
|
+
const order: MemoryFilter[] = this.callbacks.trusted() ? ["all", "project", "global"] : ["all", "global"];
|
|
154
|
+
const at = order.indexOf(this.effectiveFilter());
|
|
155
|
+
this.filter = order[(at + 1) % order.length];
|
|
156
|
+
this.selectedKey = undefined;
|
|
157
|
+
this.requestRender();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private startEdit(note: Note | undefined): void {
|
|
161
|
+
if (!note) return;
|
|
162
|
+
if (note.scope === "project" && !this.callbacks.trusted()) {
|
|
163
|
+
this.notice = "Project memory is unavailable for an untrusted project.";
|
|
164
|
+
this.requestRender();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
this.mode = "edit";
|
|
168
|
+
this.editing = note;
|
|
169
|
+
this.editorScope = note.scope;
|
|
170
|
+
this.editor.setText(note.text);
|
|
171
|
+
this.syncEditorFocus();
|
|
172
|
+
this.requestRender();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private startAdd(): void {
|
|
176
|
+
this.mode = "add";
|
|
177
|
+
this.editing = undefined;
|
|
178
|
+
this.editorScope = this.activeScope();
|
|
179
|
+
this.editor.setText("");
|
|
180
|
+
this.syncEditorFocus();
|
|
181
|
+
this.requestRender();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private cancelEditor(): void {
|
|
185
|
+
this.mode = "list";
|
|
186
|
+
this.editing = undefined;
|
|
187
|
+
this.syncEditorFocus();
|
|
188
|
+
this.notice = undefined;
|
|
189
|
+
this.requestRender();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private submitEditor(value: string): void {
|
|
193
|
+
const text = sanitizeLine(value).trim();
|
|
194
|
+
const editing = this.editing;
|
|
195
|
+
const scope = this.editorScope;
|
|
196
|
+
this.mode = "list";
|
|
197
|
+
this.editing = undefined;
|
|
198
|
+
this.syncEditorFocus();
|
|
199
|
+
|
|
200
|
+
if (text.length === 0) {
|
|
201
|
+
this.notice = "A note cannot be blank.";
|
|
202
|
+
this.requestRender();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (text.length > MAX_NOTE_CHARS) {
|
|
206
|
+
this.notice = `A note must be at most ${MAX_NOTE_CHARS} characters.`;
|
|
207
|
+
this.requestRender();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
this.run(async () => {
|
|
212
|
+
if (editing) {
|
|
213
|
+
if (text === editing.text) {
|
|
214
|
+
this.notice = "Unchanged.";
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
await this.callbacks.update(scope, editing.text, text);
|
|
218
|
+
this.notice = `Updated ${scopeLabel(scope)} note.`;
|
|
219
|
+
this.selectedKey = noteKey({ scope, text });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const result = await this.callbacks.add(scope, text);
|
|
223
|
+
if (result.changed) {
|
|
224
|
+
this.notice = `Added ${scopeLabel(scope)} note.`;
|
|
225
|
+
this.selectedKey = noteKey({ scope, text });
|
|
226
|
+
} else {
|
|
227
|
+
this.notice = `Already remembered: ${result.existing ?? text}`;
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private forgetNote(note: Note): void {
|
|
233
|
+
// Capture the neighbour before the write so the focus lands on it after.
|
|
234
|
+
const before = this.notes();
|
|
235
|
+
const at = before.findIndex((item) => noteKey(item) === noteKey(note));
|
|
236
|
+
const neighbour = before[at + 1] ?? before[at - 1];
|
|
237
|
+
this.run(async () => {
|
|
238
|
+
await this.callbacks.forget(note.scope, note.text);
|
|
239
|
+
this.notice = `Forgot ${scopeLabel(note.scope)} note.`;
|
|
240
|
+
this.selectedKey = neighbour ? noteKey(neighbour) : undefined;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private reload(): void {
|
|
245
|
+
this.notice = this.callbacks.refresh() ? "Reloaded memory." : "Memory is already up to date.";
|
|
246
|
+
this.requestRender();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Run a mutation off the shared chain, then repaint from the new state. */
|
|
250
|
+
private run(action: () => Promise<void>): void {
|
|
251
|
+
this.chain = this.chain
|
|
252
|
+
.then(action)
|
|
253
|
+
.catch((error: unknown) => {
|
|
254
|
+
this.notice = `Memory not saved: ${messageOf(error)}`;
|
|
255
|
+
})
|
|
256
|
+
.then(() => this.requestRender());
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private syncEditorFocus(): void {
|
|
260
|
+
this.editor.focused = this.componentFocused && (this.mode === "edit" || this.mode === "add");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private handleConfirm(data: string): void {
|
|
264
|
+
const note = this.confirm;
|
|
265
|
+
if (!note) return;
|
|
266
|
+
if (data === "y" || data === "Y" || matchesKey(data, Key.enter)) {
|
|
267
|
+
this.confirm = undefined;
|
|
268
|
+
this.forgetNote(note);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (data === "n" || data === "N" || isCloseKey(data) || data === "q") {
|
|
272
|
+
this.confirm = undefined;
|
|
273
|
+
this.requestRender();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
handleInput(data: string): void {
|
|
278
|
+
if (this.mode === "edit" || this.mode === "add") {
|
|
279
|
+
if (isCloseKey(data) || matchesKey(data, Key.escape)) {
|
|
280
|
+
this.cancelEditor();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
this.editor.handleInput(data);
|
|
284
|
+
this.requestRender();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (this.confirm) {
|
|
288
|
+
this.handleConfirm(data);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (isCloseKey(data) || data === "q") {
|
|
292
|
+
this.onClose();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (this.notice !== undefined) this.notice = undefined;
|
|
296
|
+
|
|
297
|
+
const notes = this.notes();
|
|
298
|
+
this.focusIndexNow(this.focusIndex(notes), notes);
|
|
299
|
+
if (navigateList(data, this.cursor, notes.length)) {
|
|
300
|
+
this.selectedKey = notes[this.cursor.selected] ? noteKey(notes[this.cursor.selected]) : undefined;
|
|
301
|
+
this.requestRender();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const focused = notes[this.cursor.selected];
|
|
306
|
+
if (matchesKey(data, Key.tab) || data === "s") this.cycleFilter();
|
|
307
|
+
else if (matchesKey(data, Key.enter) || data === "e") this.startEdit(focused);
|
|
308
|
+
else if (data === "a") this.startAdd();
|
|
309
|
+
else if (data === "d") {
|
|
310
|
+
if (!focused) return;
|
|
311
|
+
this.confirm = focused;
|
|
312
|
+
this.requestRender();
|
|
313
|
+
} else if (data === "r") this.reload();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Wheel scrolling moves the selection, matching the other list screens. */
|
|
317
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
318
|
+
if (this.mode !== "list" || this.confirm) return undefined;
|
|
319
|
+
const notes = this.notes();
|
|
320
|
+
const handled = scrollList(event, this.cursor, notes.length);
|
|
321
|
+
if (handled) this.selectedKey = notes[this.cursor.selected] ? noteKey(notes[this.cursor.selected]) : undefined;
|
|
322
|
+
return handled;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
invalidate(): void {
|
|
326
|
+
this.editor.invalidate();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
dispose(): void {
|
|
330
|
+
this.editor.focused = false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
get focused(): boolean {
|
|
334
|
+
return this.componentFocused;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
set focused(value: boolean) {
|
|
338
|
+
if (this.componentFocused === value) return;
|
|
339
|
+
this.componentFocused = value;
|
|
340
|
+
this.syncEditorFocus();
|
|
341
|
+
this.requestRender();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private summary(width: number): string {
|
|
345
|
+
const entries = this.callbacks.entries();
|
|
346
|
+
const filter = this.effectiveFilter();
|
|
347
|
+
let text = this.theme.fg("accent", filter === "all" ? "All" : scopeLabel(filter));
|
|
348
|
+
text += this.theme.fg("muted", ` · ${entries.project.length} project, ${entries.global.length} global`);
|
|
349
|
+
if (!this.callbacks.trusted()) text += this.theme.fg("warning", " · project untrusted");
|
|
350
|
+
return truncateToWidth(` ${text}`, width, "…");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** One note row: selection marker, scope tag, and the sanitized note text. */
|
|
354
|
+
private noteRow(note: Note, selected: boolean, width: number): string {
|
|
355
|
+
const scope = this.theme.fg("muted", scopeLabel(note.scope));
|
|
356
|
+
const text = sanitizeLine(note.text);
|
|
357
|
+
const label = selected ? this.theme.fg("accent", text) : this.theme.fg("text", text);
|
|
358
|
+
return truncateToWidth(
|
|
359
|
+
`${selectionMarker(this.theme, selected)}${scope} ${this.theme.fg("dim", "·")} ${label}`,
|
|
360
|
+
width,
|
|
361
|
+
"…",
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private confirmPrompt(): string {
|
|
366
|
+
const note = this.confirm;
|
|
367
|
+
if (!note) return "";
|
|
368
|
+
const label = sanitizeLine(note.text);
|
|
369
|
+
const clipped = label.length > 40 ? `${label.slice(0, 39)}…` : label;
|
|
370
|
+
return `Forget ${scopeLabel(note.scope)} note "${clipped}"? y/N`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private renderEditor(width: number): string[] {
|
|
374
|
+
const verb = this.mode === "edit" ? "Edit" : "Add";
|
|
375
|
+
const scope = scopeLabel(this.editorScope);
|
|
376
|
+
const pad = width > 1 ? " " : "";
|
|
377
|
+
const body: string[] = [];
|
|
378
|
+
for (const line of this.editor.render(Math.max(1, width - 2))) body.push(`${pad}${line}`);
|
|
379
|
+
return dockListFrame({
|
|
380
|
+
theme: this.theme,
|
|
381
|
+
width,
|
|
382
|
+
title: `Memory · ${verb} ${scope}`,
|
|
383
|
+
summary: truncateToWidth(
|
|
384
|
+
` ${this.theme.fg("muted", this.mode === "edit" ? "Editing the focused note" : `New ${scope} note`)}`,
|
|
385
|
+
width,
|
|
386
|
+
"…",
|
|
387
|
+
),
|
|
388
|
+
body,
|
|
389
|
+
hint: screenHint(this.theme, width, ["Enter save", "Esc cancel"]),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
render(width: number): string[] {
|
|
394
|
+
const w = Math.max(1, width);
|
|
395
|
+
this.syncEditorFocus();
|
|
396
|
+
if (this.mode === "edit" || this.mode === "add") return this.renderEditor(w);
|
|
397
|
+
|
|
398
|
+
const notes = this.notes();
|
|
399
|
+
this.cursor.visible = fitRows(this.viewportRowsSource, notes.length, CHROME, SCREEN_DEFAULT_ROWS);
|
|
400
|
+
this.focusIndexNow(this.focusIndex(notes), notes);
|
|
401
|
+
|
|
402
|
+
const body: string[] = [];
|
|
403
|
+
if (notes.length === 0) {
|
|
404
|
+
body.push(truncateToWidth(` ${this.theme.fg("dim", "No notes yet. Press a to add one.")}`, w));
|
|
405
|
+
} else {
|
|
406
|
+
const { start, end } = this.cursor.window(notes.length);
|
|
407
|
+
for (let index = start; index < end; index++) {
|
|
408
|
+
body.push(this.noteRow(notes[index], index === this.cursor.selected, w));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const hint = this.confirm
|
|
413
|
+
? truncateToWidth(` ${this.theme.fg("warning", this.confirmPrompt())}`, w)
|
|
414
|
+
: this.notice !== undefined
|
|
415
|
+
? truncateToWidth(` ${this.theme.fg("muted", this.notice)}`, w)
|
|
416
|
+
: screenHint(this.theme, w, ["Esc close", "Enter/e edit", "a add", "d forget", "s/Tab scope", "↑/↓", "g/G"]);
|
|
417
|
+
|
|
418
|
+
return dockListFrame({
|
|
419
|
+
theme: this.theme,
|
|
420
|
+
width: w,
|
|
421
|
+
title: "Memory",
|
|
422
|
+
summary: this.summary(w),
|
|
423
|
+
body,
|
|
424
|
+
range: this.cursor.clipped(notes.length)
|
|
425
|
+
? formatRange(this.theme, w, { ...this.cursor.window(notes.length), total: notes.length })
|
|
426
|
+
: undefined,
|
|
427
|
+
hint,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the memory extension.
|
|
3
|
+
*
|
|
4
|
+
* Memory is kept outside the session branch as human-editable markdown, so it
|
|
5
|
+
* survives `/new`, `/resume`, and `/tree`. A note is one bullet line, scoped
|
|
6
|
+
* `global` (every project) or `project` (the current repository).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const MEMORY_SCOPES = ["project", "global"] as const;
|
|
10
|
+
|
|
11
|
+
export type MemoryScope = (typeof MEMORY_SCOPES)[number];
|
|
12
|
+
|
|
13
|
+
/** What a call did, used by the result schema, details, and transcript renderer. */
|
|
14
|
+
export const MEMORY_ACTIONS = ["add", "update", "forget", "list"] as const;
|
|
15
|
+
|
|
16
|
+
export type MemoryAction = (typeof MEMORY_ACTIONS)[number];
|
|
17
|
+
|
|
18
|
+
/** Custom-message type for the hidden injected memory context. */
|
|
19
|
+
export const MEMORY_CONTEXT_TYPE = "memory-context";
|
|
20
|
+
|
|
21
|
+
/** Structured result carried in the tool's `details` and `structuredContent`. */
|
|
22
|
+
export interface MemoryDetails {
|
|
23
|
+
action: MemoryAction;
|
|
24
|
+
/** The scope the call targeted. */
|
|
25
|
+
scope: MemoryScope;
|
|
26
|
+
/** Notes after the call; empty for `project` when the project is untrusted. */
|
|
27
|
+
project: string[];
|
|
28
|
+
global: string[];
|
|
29
|
+
changed: boolean;
|
|
30
|
+
/** The note added, forgotten, or replaced (the previous text, for `update`). */
|
|
31
|
+
entry?: string;
|
|
32
|
+
/** The replacement note, for `update`. */
|
|
33
|
+
to?: string;
|
|
34
|
+
/** The stored note an `add` matched as a duplicate, when it did. */
|
|
35
|
+
existing?: string;
|
|
36
|
+
/** Model-readable validation message when the call was rejected. */
|
|
37
|
+
error?: string;
|
|
38
|
+
}
|