obsidian-catui-acp 0.1.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 +52 -0
- package/THIRD_PARTY_NOTICES.txt +3002 -0
- package/esbuild.config.mjs +29 -0
- package/main.js +3499 -0
- package/manifest.json +9 -0
- package/package.json +57 -0
- package/src/acp/catui-client.ts +108 -0
- package/src/acp/catui-process.ts +49 -0
- package/src/acp/ndjson-rpc.ts +142 -0
- package/src/acp/types.ts +101 -0
- package/src/chat/acp-chat-transport.ts +28 -0
- package/src/chat/session-messages-cache.ts +40 -0
- package/src/context/vault-context.ts +35 -0
- package/src/main.ts +529 -0
- package/src/policy/permission-policy.ts +52 -0
- package/src/react/ChatApp.tsx +677 -0
- package/src/settings.ts +124 -0
- package/src/styles.css +270 -0
- package/src/ui/chat-view.tsx +38 -0
- package/styles.css +2 -0
- package/test/catui-client.test.ts +61 -0
- package/test/ndjson-rpc.test.ts +74 -0
- package/test/permission-policy.test.ts +31 -0
- package/test/session-messages-cache.test.ts +59 -0
- package/test/vault-context.test.ts +44 -0
- package/tsconfig.json +17 -0
package/manifest.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "obsidian-catui-acp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Obsidian desktop sidebar for chatting with a local CatUI ACP agent.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "cunyu666",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"obsidian",
|
|
9
|
+
"obsidian-plugin",
|
|
10
|
+
"catui",
|
|
11
|
+
"acp",
|
|
12
|
+
"llm-wiki"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
"main.js",
|
|
16
|
+
"manifest.json",
|
|
17
|
+
"styles.css",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"THIRD_PARTY_NOTICES.txt",
|
|
20
|
+
"src/",
|
|
21
|
+
"test/",
|
|
22
|
+
"tsconfig.json",
|
|
23
|
+
"esbuild.config.mjs",
|
|
24
|
+
"package-lock.json"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"registry": "https://registry.npmjs.org/",
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"type": "module",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build:css": "tailwindcss -i ./src/styles.css -o ./styles.css --minify",
|
|
33
|
+
"build": "npm run build:css && node esbuild.config.mjs production",
|
|
34
|
+
"dev": "npm run build:css && node esbuild.config.mjs",
|
|
35
|
+
"test": "node --test --import tsx test/*.test.ts",
|
|
36
|
+
"typecheck": "tsc --noEmit"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@tailwindcss/cli": "^4.3.3",
|
|
40
|
+
"@types/node": "^22.15.0",
|
|
41
|
+
"@types/react": "^19.2.18",
|
|
42
|
+
"@types/react-dom": "^19.2.4",
|
|
43
|
+
"builtin-modules": "^5.0.0",
|
|
44
|
+
"esbuild": "^0.27.3",
|
|
45
|
+
"obsidian": "^1.8.7",
|
|
46
|
+
"tailwindcss": "^4.3.3",
|
|
47
|
+
"tsx": "^4.20.0",
|
|
48
|
+
"typescript": "^5.7.0",
|
|
49
|
+
"@ai-sdk/react": "^4.0.51",
|
|
50
|
+
"ai": "^7.0.48",
|
|
51
|
+
"lucide-react": "^0.561.0",
|
|
52
|
+
"react": "^19.2.8",
|
|
53
|
+
"react-dom": "^19.2.8",
|
|
54
|
+
"react-markdown": "^10.1.0",
|
|
55
|
+
"remark-gfm": "^4.0.1"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type {
|
|
3
|
+
AcpInitializeResult,
|
|
4
|
+
AcpListSessionsResult,
|
|
5
|
+
AcpNewSessionResult,
|
|
6
|
+
AcpPermissionRequestParams,
|
|
7
|
+
AcpSessionUpdateParams,
|
|
8
|
+
} from "./types";
|
|
9
|
+
|
|
10
|
+
export interface RpcPeerLike {
|
|
11
|
+
request<TResult = unknown>(method: string, params?: unknown): Promise<TResult>;
|
|
12
|
+
notify(method: string, params?: unknown): void;
|
|
13
|
+
onNotification(method: string, handler: (params: unknown) => void): void;
|
|
14
|
+
onRequest(method: string, handler: (params: unknown) => Promise<unknown> | unknown): void;
|
|
15
|
+
onError?(handler: (error: Error) => void): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class CatuiAcpClient extends EventEmitter {
|
|
19
|
+
sessionId: string | null = null;
|
|
20
|
+
initializeResult: AcpInitializeResult | null = null;
|
|
21
|
+
sessionState: AcpNewSessionResult | null = null;
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
private readonly peer: RpcPeerLike,
|
|
25
|
+
private readonly cwd: string,
|
|
26
|
+
) {
|
|
27
|
+
super();
|
|
28
|
+
this.peer.onNotification("session/update", (params) =>
|
|
29
|
+
this.emit("session-update", params as AcpSessionUpdateParams),
|
|
30
|
+
);
|
|
31
|
+
this.peer.onRequest("session/request_permission", (params) =>
|
|
32
|
+
this.emitPermission(params as AcpPermissionRequestParams),
|
|
33
|
+
);
|
|
34
|
+
this.peer.onError?.((error) => this.emit("error", error));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async initialize(): Promise<AcpInitializeResult> {
|
|
38
|
+
this.initializeResult = await this.peer.request<AcpInitializeResult>("initialize", {
|
|
39
|
+
protocolVersion: 1,
|
|
40
|
+
clientCapabilities: {},
|
|
41
|
+
});
|
|
42
|
+
return this.initializeResult;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async newSession(): Promise<AcpNewSessionResult> {
|
|
46
|
+
const result = await this.peer.request<AcpNewSessionResult>("session/new", {
|
|
47
|
+
cwd: this.cwd,
|
|
48
|
+
mcpServers: [],
|
|
49
|
+
});
|
|
50
|
+
this.sessionId = result.sessionId;
|
|
51
|
+
this.sessionState = result;
|
|
52
|
+
this.emit("session-ready", result);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async listSessions(): Promise<AcpListSessionsResult> {
|
|
57
|
+
return this.peer.request<AcpListSessionsResult>("session/list", { cwd: this.cwd });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async loadSession(sessionId: string): Promise<unknown> {
|
|
61
|
+
const result = await this.peer.request("session/load", {
|
|
62
|
+
sessionId,
|
|
63
|
+
cwd: this.cwd,
|
|
64
|
+
mcpServers: [],
|
|
65
|
+
});
|
|
66
|
+
this.sessionId = sessionId;
|
|
67
|
+
this.emit("session-ready", result);
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async closeSession(sessionId: string): Promise<unknown> {
|
|
72
|
+
const result = await this.peer.request("session/close", { sessionId });
|
|
73
|
+
if (this.sessionId === sessionId) {
|
|
74
|
+
this.sessionId = null;
|
|
75
|
+
this.sessionState = null;
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async setMode(modeId: string): Promise<void> {
|
|
81
|
+
if (!this.sessionId) throw new Error("No ACP session is active");
|
|
82
|
+
await this.peer.request("session/set_mode", { sessionId: this.sessionId, modeId });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async setModel(modelId: string): Promise<void> {
|
|
86
|
+
if (!this.sessionId) throw new Error("No ACP session is active");
|
|
87
|
+
await this.peer.request("session/set_model", { sessionId: this.sessionId, modelId });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async prompt(text: string): Promise<unknown> {
|
|
91
|
+
if (!this.sessionId) throw new Error("No ACP session is active");
|
|
92
|
+
return this.peer.request("session/prompt", {
|
|
93
|
+
sessionId: this.sessionId,
|
|
94
|
+
prompt: [{ type: "text", text }],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
cancel(): void {
|
|
99
|
+
if (!this.sessionId) return;
|
|
100
|
+
this.peer.notify("session/cancel", { sessionId: this.sessionId });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private emitPermission(params: AcpPermissionRequestParams): Promise<unknown> {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
this.emit("permission-request", params, resolve);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
|
|
4
|
+
export interface CatuiProcessOptions {
|
|
5
|
+
command: string;
|
|
6
|
+
args: string[];
|
|
7
|
+
cwd: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class CatuiProcess extends EventEmitter {
|
|
11
|
+
private child: ChildProcessWithoutNullStreams | null = null;
|
|
12
|
+
stderrText = "";
|
|
13
|
+
|
|
14
|
+
get running(): boolean {
|
|
15
|
+
return this.child !== null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
start(options: CatuiProcessOptions): ChildProcessWithoutNullStreams {
|
|
19
|
+
this.stop();
|
|
20
|
+
this.stderrText = "";
|
|
21
|
+
this.child = spawn(options.command, options.args, {
|
|
22
|
+
cwd: options.cwd,
|
|
23
|
+
env: process.env,
|
|
24
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
this.child.stderr.on("data", (chunk: Buffer) => {
|
|
28
|
+
const text = chunk.toString("utf8");
|
|
29
|
+
this.stderrText += text;
|
|
30
|
+
this.emit("stderr", text);
|
|
31
|
+
});
|
|
32
|
+
this.child.on("exit", (code, signal) => {
|
|
33
|
+
this.emit("exit", { code, signal });
|
|
34
|
+
this.child = null;
|
|
35
|
+
});
|
|
36
|
+
this.child.on("error", (error) => {
|
|
37
|
+
this.emit("error", error);
|
|
38
|
+
this.child = null;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return this.child;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
stop(): void {
|
|
45
|
+
if (!this.child) return;
|
|
46
|
+
this.child.kill();
|
|
47
|
+
this.child = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
import type {
|
|
3
|
+
JsonRpcFailure,
|
|
4
|
+
JsonRpcId,
|
|
5
|
+
JsonRpcMessage,
|
|
6
|
+
JsonRpcRequest,
|
|
7
|
+
JsonRpcSuccess,
|
|
8
|
+
} from "./types";
|
|
9
|
+
|
|
10
|
+
type Pending = {
|
|
11
|
+
resolve: (value: unknown) => void;
|
|
12
|
+
reject: (error: Error) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type RequestHandler = (params: unknown) => Promise<unknown> | unknown;
|
|
16
|
+
type NotificationHandler = (params: unknown) => void;
|
|
17
|
+
type ErrorHandler = (error: Error) => void;
|
|
18
|
+
|
|
19
|
+
export class NdjsonRpcPeer {
|
|
20
|
+
private nextId = 1;
|
|
21
|
+
private buffer = "";
|
|
22
|
+
private readonly pending = new Map<JsonRpcId, Pending>();
|
|
23
|
+
private readonly requestHandlers = new Map<string, RequestHandler>();
|
|
24
|
+
private readonly notificationHandlers = new Map<string, NotificationHandler[]>();
|
|
25
|
+
private readonly errorHandlers: ErrorHandler[] = [];
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
private readonly input: Readable,
|
|
29
|
+
private readonly output: Writable,
|
|
30
|
+
) {
|
|
31
|
+
this.input.setEncoding("utf8");
|
|
32
|
+
this.input.on("data", (chunk: string) => this.accept(chunk));
|
|
33
|
+
this.input.on("error", (error) => this.fail(error instanceof Error ? error : new Error(String(error))));
|
|
34
|
+
this.input.on("close", () => this.rejectAll(new Error("ACP stream closed")));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
request<TResult = unknown>(method: string, params?: unknown): Promise<TResult> {
|
|
38
|
+
const id = this.nextId++;
|
|
39
|
+
const message: JsonRpcRequest = { jsonrpc: "2.0", id, method, params };
|
|
40
|
+
this.write(message);
|
|
41
|
+
return new Promise<TResult>((resolve, reject) => {
|
|
42
|
+
this.pending.set(id, { resolve: resolve as (value: unknown) => void, reject });
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
notify(method: string, params?: unknown): void {
|
|
47
|
+
this.write({ jsonrpc: "2.0", method, params });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onRequest(method: string, handler: RequestHandler): void {
|
|
51
|
+
this.requestHandlers.set(method, handler);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
onNotification(method: string, handler: NotificationHandler): void {
|
|
55
|
+
const handlers = this.notificationHandlers.get(method) ?? [];
|
|
56
|
+
handlers.push(handler);
|
|
57
|
+
this.notificationHandlers.set(method, handlers);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
onError(handler: ErrorHandler): void {
|
|
61
|
+
this.errorHandlers.push(handler);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private accept(chunk: string): void {
|
|
65
|
+
this.buffer += chunk;
|
|
66
|
+
for (;;) {
|
|
67
|
+
const newline = this.buffer.indexOf("\n");
|
|
68
|
+
if (newline === -1) return;
|
|
69
|
+
const line = this.buffer.slice(0, newline).trim();
|
|
70
|
+
this.buffer = this.buffer.slice(newline + 1);
|
|
71
|
+
if (line) this.handleLine(line);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private handleLine(line: string): void {
|
|
76
|
+
let message: JsonRpcMessage;
|
|
77
|
+
try {
|
|
78
|
+
message = JSON.parse(line) as JsonRpcMessage;
|
|
79
|
+
} catch {
|
|
80
|
+
for (const handler of this.errorHandlers) {
|
|
81
|
+
handler(new Error(`Ignored non-ACP output: ${line.slice(0, 120)}`));
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if ("id" in message && typeof message.id === "number" && ("result" in message || "error" in message)) {
|
|
87
|
+
this.handleResponse(message as JsonRpcSuccess | JsonRpcFailure);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if ("id" in message && typeof message.id === "number" && "method" in message && typeof message.method === "string") {
|
|
92
|
+
void this.handleRequest(message.id, message.method, message.params);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if ("method" in message && typeof message.method === "string") {
|
|
97
|
+
for (const handler of this.notificationHandlers.get(message.method) ?? []) {
|
|
98
|
+
handler(message.params);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private handleResponse(message: JsonRpcSuccess | JsonRpcFailure): void {
|
|
104
|
+
const pending = this.pending.get(message.id);
|
|
105
|
+
if (!pending) return;
|
|
106
|
+
this.pending.delete(message.id);
|
|
107
|
+
if ("error" in message) {
|
|
108
|
+
pending.reject(new Error(message.error.message));
|
|
109
|
+
} else {
|
|
110
|
+
pending.resolve(message.result);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private async handleRequest(id: number, method: string, params: unknown): Promise<void> {
|
|
115
|
+
const handler = this.requestHandlers.get(method);
|
|
116
|
+
if (!handler) {
|
|
117
|
+
this.write({ jsonrpc: "2.0", id, error: { code: -32601, message: `No handler for ${method}` } });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
this.write({ jsonrpc: "2.0", id, result: await handler(params) });
|
|
123
|
+
} catch (error) {
|
|
124
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
125
|
+
this.write({ jsonrpc: "2.0", id, error: { code: -32000, message } });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private write(message: unknown): void {
|
|
130
|
+
this.output.write(`${JSON.stringify(message)}\n`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private fail(error: Error): void {
|
|
134
|
+
for (const handler of this.errorHandlers) handler(error);
|
|
135
|
+
this.rejectAll(error);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private rejectAll(error: Error): void {
|
|
139
|
+
for (const pending of this.pending.values()) pending.reject(error);
|
|
140
|
+
this.pending.clear();
|
|
141
|
+
}
|
|
142
|
+
}
|
package/src/acp/types.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export type JsonRpcId = number;
|
|
2
|
+
|
|
3
|
+
export interface JsonRpcRequest<TParams = unknown> {
|
|
4
|
+
jsonrpc: "2.0";
|
|
5
|
+
id: JsonRpcId;
|
|
6
|
+
method: string;
|
|
7
|
+
params?: TParams;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface JsonRpcNotification<TParams = unknown> {
|
|
11
|
+
jsonrpc: "2.0";
|
|
12
|
+
method: string;
|
|
13
|
+
params?: TParams;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface JsonRpcSuccess<TResult = unknown> {
|
|
17
|
+
jsonrpc: "2.0";
|
|
18
|
+
id: JsonRpcId;
|
|
19
|
+
result: TResult;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface JsonRpcFailure {
|
|
23
|
+
jsonrpc: "2.0";
|
|
24
|
+
id: JsonRpcId;
|
|
25
|
+
error: {
|
|
26
|
+
code: number;
|
|
27
|
+
message: string;
|
|
28
|
+
data?: unknown;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type JsonRpcMessage =
|
|
33
|
+
| JsonRpcRequest
|
|
34
|
+
| JsonRpcNotification
|
|
35
|
+
| JsonRpcSuccess
|
|
36
|
+
| JsonRpcFailure;
|
|
37
|
+
|
|
38
|
+
export interface AcpTextContentBlock {
|
|
39
|
+
type: "text";
|
|
40
|
+
text: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AcpInitializeResult {
|
|
44
|
+
protocolVersion: number;
|
|
45
|
+
agentInfo: { name: string; version: string };
|
|
46
|
+
agentCapabilities: Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface AcpModeInfo {
|
|
50
|
+
id: string;
|
|
51
|
+
name: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AcpModelInfo {
|
|
56
|
+
modelId: string;
|
|
57
|
+
name?: string;
|
|
58
|
+
description?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface AcpNewSessionResult {
|
|
62
|
+
sessionId: string;
|
|
63
|
+
modes?: {
|
|
64
|
+
currentModeId?: string;
|
|
65
|
+
availableModes?: AcpModeInfo[];
|
|
66
|
+
};
|
|
67
|
+
models?: {
|
|
68
|
+
currentModelId?: string;
|
|
69
|
+
availableModels?: AcpModelInfo[];
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface AcpListSessionsResult {
|
|
74
|
+
sessions: Array<{
|
|
75
|
+
sessionId: string;
|
|
76
|
+
cwd?: string;
|
|
77
|
+
title?: string;
|
|
78
|
+
updatedAt?: string;
|
|
79
|
+
}>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface AcpSessionUpdateParams {
|
|
83
|
+
sessionId: string;
|
|
84
|
+
update: Record<string, unknown> & { sessionUpdate: string };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface AcpPermissionRequestParams {
|
|
88
|
+
sessionId: string;
|
|
89
|
+
options: Array<{ optionId: string; name: string; kind?: string }>;
|
|
90
|
+
toolCall: {
|
|
91
|
+
toolCallId: string;
|
|
92
|
+
title: string;
|
|
93
|
+
kind?: string;
|
|
94
|
+
status?: string;
|
|
95
|
+
rawInput?: unknown;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type AcpPermissionResponse =
|
|
100
|
+
| { outcome: { outcome: "selected"; optionId: string } }
|
|
101
|
+
| { outcome: { outcome: "cancelled" } };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ChatTransport, UIMessage, UIMessageChunk } from "ai";
|
|
2
|
+
import type CatuiAcpPlugin from "../main";
|
|
3
|
+
|
|
4
|
+
export class AcpChatTransport implements ChatTransport<UIMessage> {
|
|
5
|
+
constructor(private readonly plugin: CatuiAcpPlugin) {}
|
|
6
|
+
|
|
7
|
+
async sendMessages(options: Parameters<ChatTransport<UIMessage>["sendMessages"]>[0]): Promise<ReadableStream<UIMessageChunk>> {
|
|
8
|
+
const text = getLastUserText(options.messages);
|
|
9
|
+
if (!text) throw new Error("Cannot send an empty message.");
|
|
10
|
+
return this.plugin.runPromptAsStream(text, options.abortSignal);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async reconnectToStream(): Promise<ReadableStream<UIMessageChunk> | null> {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getLastUserText(messages: UIMessage[]): string {
|
|
19
|
+
const message = [...messages].reverse().find((item) => item.role === "user");
|
|
20
|
+
if (!message) return "";
|
|
21
|
+
return message.parts
|
|
22
|
+
.map((part) => {
|
|
23
|
+
if (part.type === "text") return part.text;
|
|
24
|
+
return "";
|
|
25
|
+
})
|
|
26
|
+
.join("\n")
|
|
27
|
+
.trim();
|
|
28
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { UIMessage } from "ai";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_CAPACITY = 10;
|
|
4
|
+
|
|
5
|
+
export class SessionMessagesCache {
|
|
6
|
+
private readonly entries = new Map<string, UIMessage[]>();
|
|
7
|
+
|
|
8
|
+
constructor(private readonly capacity = DEFAULT_CAPACITY) {}
|
|
9
|
+
|
|
10
|
+
save(sessionId: string, messages: UIMessage[]): void {
|
|
11
|
+
if (messages.length === 0) return;
|
|
12
|
+
this.entries.delete(sessionId);
|
|
13
|
+
this.entries.set(sessionId, [...messages]);
|
|
14
|
+
while (this.entries.size > this.capacity) {
|
|
15
|
+
const oldest = this.entries.keys().next().value;
|
|
16
|
+
if (oldest === undefined) break;
|
|
17
|
+
this.entries.delete(oldest);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
load(sessionId: string): UIMessage[] | null {
|
|
22
|
+
const messages = this.entries.get(sessionId);
|
|
23
|
+
if (!messages) return null;
|
|
24
|
+
this.entries.delete(sessionId);
|
|
25
|
+
this.entries.set(sessionId, messages);
|
|
26
|
+
return [...messages];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
evict(sessionId: string): void {
|
|
30
|
+
this.entries.delete(sessionId);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
clear(): void {
|
|
34
|
+
this.entries.clear();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get size(): number {
|
|
38
|
+
return this.entries.size;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface VaultPromptContext {
|
|
2
|
+
userText: string;
|
|
3
|
+
activePath: string | null;
|
|
4
|
+
selection: string;
|
|
5
|
+
includeActiveNote: boolean;
|
|
6
|
+
includeSelection: boolean;
|
|
7
|
+
personality?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function buildPromptWithVaultContext(context: VaultPromptContext): string {
|
|
11
|
+
const lines = [
|
|
12
|
+
context.userText.trim(),
|
|
13
|
+
"",
|
|
14
|
+
"Vault context:",
|
|
15
|
+
"- This vault is a Karpathy-style LLM-Wiki.",
|
|
16
|
+
"- raw/ is immutable source material. Do not modify raw/.",
|
|
17
|
+
"- wiki/ is agent-maintained compiled knowledge.",
|
|
18
|
+
"- wiki/log.md is append-only.",
|
|
19
|
+
"- Follow AGENTS.md and skills/karpathy-llm-wiki/SKILL.md when editing.",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
if (context.personality?.trim()) {
|
|
23
|
+
lines.push(`- CatUI personality: ${context.personality.trim()}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (context.includeActiveNote && context.activePath) {
|
|
27
|
+
lines.push(`- Active note: ${context.activePath}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (context.includeSelection && context.selection.trim()) {
|
|
31
|
+
lines.push("", "Selected text:", context.selection.trim());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return lines.join("\n");
|
|
35
|
+
}
|