@pencil-agent/nano-pencil 1.11.13 → 1.11.15
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/dist/core/extensions/runner.js +4 -1
- package/dist/core/mcp/mcp-client.js +3 -3
- package/dist/core/prompt/system-prompt.js +1 -1
- package/dist/core/runtime/agent-session.d.ts +11 -0
- package/dist/core/runtime/agent-session.js +38 -0
- package/dist/core/tools/index.js +1 -1
- package/dist/core/tools/time.js +1 -1
- package/dist/extensions/defaults/soul/index.js +1 -1
- package/dist/extensions/optional/export-html/index.js +2 -2
- package/dist/main.js +47 -1
- package/dist/modes/acp/acp-mode.d.ts +5 -5
- package/dist/modes/acp/acp-mode.js +670 -79
- package/package.json +1 -1
|
@@ -195,7 +195,10 @@ export class ExtensionRunner {
|
|
|
195
195
|
return this.uiContext;
|
|
196
196
|
}
|
|
197
197
|
hasUI() {
|
|
198
|
-
|
|
198
|
+
if (this.uiContext === noOpUIContext) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
return this.uiContext.__nonInteractive !== true;
|
|
199
202
|
}
|
|
200
203
|
getExtensionPaths() {
|
|
201
204
|
return this.extensions.map((e) => e.path);
|
|
@@ -9,13 +9,14 @@ import { existsSync, readFileSync } from "fs";
|
|
|
9
9
|
import { join } from "path";
|
|
10
10
|
import { getAgentDir } from "../../config.js";
|
|
11
11
|
import { AuthStorage } from "../config/auth-storage.js";
|
|
12
|
+
import { getMCPConfigPath } from "./mcp-config.js";
|
|
12
13
|
// Log level control: DEBUG shows all MCP messages, RELEASE only shows summary
|
|
13
14
|
// Check if running from installed location (production) vs development
|
|
14
15
|
const isProductionBuild = typeof import.meta.url === "string" && import.meta.url.includes("node_modules");
|
|
15
16
|
const isDebugMode = process.env.NODE_ENV === "development" || (process.env.NODE_ENV !== "production" && !isProductionBuild);
|
|
16
17
|
function mcpLog(...args) {
|
|
17
18
|
if (isDebugMode) {
|
|
18
|
-
console.
|
|
19
|
+
console.error(...args);
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
function mcpWarn(...args) {
|
|
@@ -45,8 +46,7 @@ export class MCPClient {
|
|
|
45
46
|
* Load MCP server configurations from config file
|
|
46
47
|
*/
|
|
47
48
|
loadServersFromConfig() {
|
|
48
|
-
const
|
|
49
|
-
const configPath = join(configDir, "mcp.json");
|
|
49
|
+
const configPath = getMCPConfigPath();
|
|
50
50
|
if (!existsSync(configPath)) {
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
@@ -21,7 +21,7 @@ export function buildSystemPrompt(options = {}) {
|
|
|
21
21
|
second: "2-digit",
|
|
22
22
|
timeZoneName: "short",
|
|
23
23
|
});
|
|
24
|
-
const timeReasoningInstruction = "\nFor exact current time or date-sensitive reasoning, use the `time` tool
|
|
24
|
+
const timeReasoningInstruction = "\nFor exact current time or any date-sensitive reasoning, you must use the `time` tool before answering. This includes questions about the current time, current date, today, tomorrow, yesterday, this week, deadlines, elapsed time, or anything that depends on the real system clock. Do not rely only on this prompt timestamp for those answers.";
|
|
25
25
|
const appendSection = appendSystemPrompt ? `\n\n${appendSystemPrompt}` : "";
|
|
26
26
|
const contextFiles = providedContextFiles ?? [];
|
|
27
27
|
const skills = providedSkills ?? [];
|
|
@@ -26,6 +26,7 @@ import { type PromptTemplate } from "../prompt/prompt-templates.js";
|
|
|
26
26
|
import type { ResourceLoader } from "../config/resource-loader.js";
|
|
27
27
|
import type { BranchSummaryEntry, SessionManager } from "../session/session-manager.js";
|
|
28
28
|
import type { SettingsManager } from "../config/settings-manager.js";
|
|
29
|
+
import { type SlashCommandInfo } from "../slash-commands.js";
|
|
29
30
|
import type { BashOperations } from "../tools/bash.js";
|
|
30
31
|
/** Parsed skill block from a user message */
|
|
31
32
|
export interface ParsedSkillBlock {
|
|
@@ -138,6 +139,11 @@ export interface SessionStats {
|
|
|
138
139
|
};
|
|
139
140
|
cost: number;
|
|
140
141
|
}
|
|
142
|
+
export interface SessionSlashCommandDescriptor {
|
|
143
|
+
name: string;
|
|
144
|
+
description?: string;
|
|
145
|
+
source: "builtin" | SlashCommandInfo["source"];
|
|
146
|
+
}
|
|
141
147
|
export declare class AgentSession {
|
|
142
148
|
readonly agent: Agent;
|
|
143
149
|
readonly sessionManager: SessionManager;
|
|
@@ -211,6 +217,11 @@ export declare class AgentSession {
|
|
|
211
217
|
/** Model registry for API key resolution and model discovery */
|
|
212
218
|
get modelRegistry(): ModelRegistry;
|
|
213
219
|
get cwd(): string;
|
|
220
|
+
/**
|
|
221
|
+
* Return all currently available slash-like commands for the session.
|
|
222
|
+
* Includes built-in commands, extension commands, prompt templates, and skills.
|
|
223
|
+
*/
|
|
224
|
+
getSlashCommands(): SessionSlashCommandDescriptor[];
|
|
214
225
|
/** Emit an event to all listeners */
|
|
215
226
|
private _emit;
|
|
216
227
|
private _lastAssistantMessage;
|
|
@@ -219,6 +219,44 @@ export class AgentSession {
|
|
|
219
219
|
get cwd() {
|
|
220
220
|
return this._cwd;
|
|
221
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Return all currently available slash-like commands for the session.
|
|
224
|
+
* Includes built-in commands, extension commands, prompt templates, and skills.
|
|
225
|
+
*/
|
|
226
|
+
getSlashCommands() {
|
|
227
|
+
const builtins = BUILTIN_SLASH_COMMANDS.map((command) => ({
|
|
228
|
+
name: command.name,
|
|
229
|
+
description: command.description,
|
|
230
|
+
source: "builtin",
|
|
231
|
+
}));
|
|
232
|
+
const reservedBuiltins = new Set(BUILTIN_SLASH_COMMANDS.map((command) => command.name));
|
|
233
|
+
const extensionCommands = this._extensionRunner
|
|
234
|
+
?.getRegisteredCommandsWithPaths()
|
|
235
|
+
.filter(({ command }) => !reservedBuiltins.has(command.name))
|
|
236
|
+
.map(({ command }) => ({
|
|
237
|
+
name: command.name,
|
|
238
|
+
description: command.description,
|
|
239
|
+
source: "extension",
|
|
240
|
+
})) ?? [];
|
|
241
|
+
const promptCommands = this.promptTemplates.map((template) => ({
|
|
242
|
+
name: template.name,
|
|
243
|
+
description: template.description,
|
|
244
|
+
source: "prompt",
|
|
245
|
+
}));
|
|
246
|
+
const skillCommands = this._resourceLoader
|
|
247
|
+
.getSkills()
|
|
248
|
+
.skills.map((skill) => ({
|
|
249
|
+
name: `skill:${skill.name}`,
|
|
250
|
+
description: skill.description,
|
|
251
|
+
source: "skill",
|
|
252
|
+
}));
|
|
253
|
+
return [
|
|
254
|
+
...builtins,
|
|
255
|
+
...extensionCommands,
|
|
256
|
+
...promptCommands,
|
|
257
|
+
...skillCommands,
|
|
258
|
+
];
|
|
259
|
+
}
|
|
222
260
|
// =========================================================================
|
|
223
261
|
// Event Subscription
|
|
224
262
|
// =========================================================================
|
package/dist/core/tools/index.js
CHANGED
|
@@ -80,7 +80,7 @@ export const toolGuidance = {
|
|
|
80
80
|
ls: "列出目录内容。",
|
|
81
81
|
};
|
|
82
82
|
toolGuidance.time =
|
|
83
|
-
"Get the real current system time.
|
|
83
|
+
"Get the real current system time. You must use this for current time/date questions, today/tomorrow/yesterday, deadlines, schedules, or any temporal reasoning that depends on the live system clock.";
|
|
84
84
|
/**
|
|
85
85
|
* Get guidance for a specific tool
|
|
86
86
|
*/
|
package/dist/core/tools/time.js
CHANGED
|
@@ -34,7 +34,7 @@ export function createTimeTool() {
|
|
|
34
34
|
return {
|
|
35
35
|
name: "time",
|
|
36
36
|
label: "time",
|
|
37
|
-
description: "Get the current system time.
|
|
37
|
+
description: "Get the current system time. You must use this for time-sensitive questions such as 'what time is it', 'what day is it', 'today', 'tomorrow', 'yesterday', deadlines, schedules, elapsed time, or any request that depends on the real current date/time instead of prompt context.",
|
|
38
38
|
parameters: timeSchema,
|
|
39
39
|
execute: async (_toolCallId, { timeZone, locale }) => {
|
|
40
40
|
return {
|
|
@@ -266,7 +266,7 @@ export default async function soulExtension(pi) {
|
|
|
266
266
|
console.warn("[soul] Failed to initialize Soul manager.");
|
|
267
267
|
return;
|
|
268
268
|
}
|
|
269
|
-
console.
|
|
269
|
+
console.error("[soul] Soul extension loaded successfully.");
|
|
270
270
|
// Register event handlers
|
|
271
271
|
// agent_start: Initialize personality
|
|
272
272
|
pi.on("agent_start", async (_event, _ctx) => {
|
|
@@ -243,9 +243,9 @@ export default async function exportHtmlExtension(pi) {
|
|
|
243
243
|
// Export the session (use default output path)
|
|
244
244
|
// Pass undefined for state since we don't have access to it here
|
|
245
245
|
const filePath = await extExportSessionToHtml(sessionManager, undefined);
|
|
246
|
-
console.
|
|
246
|
+
console.error(`Session exported to: ${filePath}`);
|
|
247
247
|
},
|
|
248
248
|
});
|
|
249
|
-
console.
|
|
249
|
+
console.error("[export-html] Extension loaded");
|
|
250
250
|
}
|
|
251
251
|
//# sourceMappingURL=index.js.map
|
package/dist/main.js
CHANGED
|
@@ -695,7 +695,53 @@ export async function main(args) {
|
|
|
695
695
|
}
|
|
696
696
|
if (parsed.acp) {
|
|
697
697
|
const { runAcpMode } = await import("./modes/acp/acp-mode.js");
|
|
698
|
-
|
|
698
|
+
const createAcpSessionForCwd = async (workspaceCwd) => {
|
|
699
|
+
const resolvedWorkspaceCwd = resolveWorkingDirectory(workspaceCwd);
|
|
700
|
+
const workspaceSettingsManager = SettingsManager.create(resolvedWorkspaceCwd, agentDir);
|
|
701
|
+
reportSettingsErrors(workspaceSettingsManager, "acp startup");
|
|
702
|
+
const workspaceResourceLoader = new DefaultResourceLoader({
|
|
703
|
+
cwd: resolvedWorkspaceCwd,
|
|
704
|
+
agentDir,
|
|
705
|
+
settingsManager: workspaceSettingsManager,
|
|
706
|
+
additionalExtensionPaths: [...defaultExtPaths, ...(parsed.extensions ?? [])],
|
|
707
|
+
additionalSkillPaths: parsed.skills,
|
|
708
|
+
additionalPromptTemplatePaths: parsed.promptTemplates,
|
|
709
|
+
additionalThemePaths: parsed.themes,
|
|
710
|
+
noExtensions: parsed.noExtensions,
|
|
711
|
+
noSkills: parsed.noSkills,
|
|
712
|
+
noPromptTemplates: parsed.noPromptTemplates,
|
|
713
|
+
noThemes: parsed.noThemes,
|
|
714
|
+
systemPrompt: parsed.systemPrompt,
|
|
715
|
+
appendSystemPrompt: parsed.appendSystemPrompt,
|
|
716
|
+
});
|
|
717
|
+
await workspaceResourceLoader.reload();
|
|
718
|
+
const workspaceExtensionsResult = workspaceResourceLoader.getExtensions();
|
|
719
|
+
for (const { path, error } of workspaceExtensionsResult.errors) {
|
|
720
|
+
console.error(chalk.red(`Failed to load extension "${path}": ${error}`));
|
|
721
|
+
}
|
|
722
|
+
for (const { name, config } of workspaceExtensionsResult.runtime.pendingProviderRegistrations) {
|
|
723
|
+
modelRegistry.registerProvider(name, config);
|
|
724
|
+
}
|
|
725
|
+
workspaceExtensionsResult.runtime.pendingProviderRegistrations = [];
|
|
726
|
+
let workspaceScopedModels = [];
|
|
727
|
+
if (modelPatterns && modelPatterns.length > 0) {
|
|
728
|
+
workspaceScopedModels = await resolveModelScope(modelPatterns, modelRegistry);
|
|
729
|
+
}
|
|
730
|
+
const workspaceSessionManager = parsed.noSession
|
|
731
|
+
? SessionManager.inMemory(resolvedWorkspaceCwd)
|
|
732
|
+
: SessionManager.create(resolvedWorkspaceCwd, parsed.sessionDir);
|
|
733
|
+
const { options: workspaceSessionOptions } = buildSessionOptions(parsed, workspaceScopedModels, workspaceSessionManager, modelRegistry, workspaceSettingsManager);
|
|
734
|
+
workspaceSessionOptions.cwd = resolvedWorkspaceCwd;
|
|
735
|
+
workspaceSessionOptions.authStorage = authStorage;
|
|
736
|
+
workspaceSessionOptions.modelRegistry = modelRegistry;
|
|
737
|
+
workspaceSessionOptions.resourceLoader = workspaceResourceLoader;
|
|
738
|
+
workspaceSessionOptions.settingsManager = workspaceSettingsManager;
|
|
739
|
+
workspaceSessionOptions.enableMCP = sessionOptions.enableMCP;
|
|
740
|
+
workspaceSessionOptions.enableSoul = sessionOptions.enableSoul;
|
|
741
|
+
const { session: workspaceSession } = await createAgentSession(workspaceSessionOptions);
|
|
742
|
+
return workspaceSession;
|
|
743
|
+
};
|
|
744
|
+
await runAcpMode(session, { createSessionForCwd: createAcpSessionForCwd });
|
|
699
745
|
}
|
|
700
746
|
else if (mode === "rpc") {
|
|
701
747
|
await runRpcMode(session);
|
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Used for integrating with ACP-compatible editors like Zed and JetBrains.
|
|
5
5
|
* Communication via stdin/stdout using JSON-RPC 2.0 messages.
|
|
6
|
-
*
|
|
7
|
-
* Protocol:
|
|
8
|
-
* - Client → Agent: initialize, session/new, session/prompt, session/cancel
|
|
9
|
-
* - Agent → Client: session/update (streaming events), session/request_permission
|
|
10
6
|
*/
|
|
11
7
|
import type { AgentSession } from "../../core/runtime/agent-session.js";
|
|
8
|
+
interface AcpModeOptions {
|
|
9
|
+
createSessionForCwd?: (cwd: string) => Promise<AgentSession>;
|
|
10
|
+
}
|
|
12
11
|
/**
|
|
13
12
|
* Run in ACP mode.
|
|
14
13
|
* Listens for JSON-RPC 2.0 messages on stdin, outputs JSON-RPC responses/events on stdout.
|
|
15
14
|
*/
|
|
16
|
-
export declare function runAcpMode(session: AgentSession): Promise<never>;
|
|
15
|
+
export declare function runAcpMode(session: AgentSession, options?: AcpModeOptions): Promise<never>;
|
|
16
|
+
export {};
|
|
17
17
|
//# sourceMappingURL=acp-mode.d.ts.map
|
|
@@ -3,14 +3,30 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Used for integrating with ACP-compatible editors like Zed and JetBrains.
|
|
5
5
|
* Communication via stdin/stdout using JSON-RPC 2.0 messages.
|
|
6
|
-
*
|
|
7
|
-
* Protocol:
|
|
8
|
-
* - Client → Agent: initialize, session/new, session/prompt, session/cancel
|
|
9
|
-
* - Agent → Client: session/update (streaming events), session/request_permission
|
|
10
6
|
*/
|
|
11
7
|
import * as acp from "@agentclientprotocol/sdk";
|
|
8
|
+
import { SessionManager } from "../../core/session/session-manager.js";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
12
10
|
import { Readable, Writable } from "node:stream";
|
|
13
11
|
import { theme } from "../interactive/theme/theme.js";
|
|
12
|
+
const ACP_MODES = [
|
|
13
|
+
{
|
|
14
|
+
id: "ask",
|
|
15
|
+
name: "Ask before write",
|
|
16
|
+
description: "Request permission before mutating files or running commands.",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "read-only",
|
|
20
|
+
name: "Read-only",
|
|
21
|
+
description: "Disable editing tools and command execution.",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "bypass",
|
|
25
|
+
name: "Bypass permissions",
|
|
26
|
+
description: "Allow normal coding actions without permission prompts.",
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
const MUTATING_TOOL_NAMES = new Set(["edit", "write", "bash"]);
|
|
14
30
|
/**
|
|
15
31
|
* Map nanoPencil tool names to ACP tool kinds.
|
|
16
32
|
*/
|
|
@@ -34,16 +50,78 @@ function mapToolKind(toolName) {
|
|
|
34
50
|
return "other";
|
|
35
51
|
}
|
|
36
52
|
}
|
|
53
|
+
function createMessageId() {
|
|
54
|
+
return randomUUID();
|
|
55
|
+
}
|
|
56
|
+
function textToContent(text) {
|
|
57
|
+
return { type: "text", text };
|
|
58
|
+
}
|
|
59
|
+
function asText(value) {
|
|
60
|
+
if (typeof value === "string")
|
|
61
|
+
return value;
|
|
62
|
+
try {
|
|
63
|
+
return JSON.stringify(value, null, 2);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return String(value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function createSlashCommandsUpdate(session) {
|
|
70
|
+
const commands = session.getSlashCommands();
|
|
71
|
+
const seen = new Set();
|
|
72
|
+
return commands
|
|
73
|
+
.filter((command) => {
|
|
74
|
+
const key = command.name.toLowerCase();
|
|
75
|
+
if (seen.has(key))
|
|
76
|
+
return false;
|
|
77
|
+
seen.add(key);
|
|
78
|
+
return true;
|
|
79
|
+
})
|
|
80
|
+
.map((command) => ({
|
|
81
|
+
name: `/${command.name}`,
|
|
82
|
+
description: command.description ?? `Run /${command.name}`,
|
|
83
|
+
input: { hint: "Enter command arguments" },
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
function getMessageText(message) {
|
|
87
|
+
if (!("content" in message) || !Array.isArray(message.content)) {
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
90
|
+
return message.content
|
|
91
|
+
.map((block) => {
|
|
92
|
+
if (typeof block === "string")
|
|
93
|
+
return block;
|
|
94
|
+
if ("type" in block && block.type === "text")
|
|
95
|
+
return block.text;
|
|
96
|
+
return "";
|
|
97
|
+
})
|
|
98
|
+
.filter((part) => part.length > 0)
|
|
99
|
+
.join("\n");
|
|
100
|
+
}
|
|
101
|
+
function isMutatingTool(tool) {
|
|
102
|
+
if (MUTATING_TOOL_NAMES.has(tool.name))
|
|
103
|
+
return true;
|
|
104
|
+
if (/^mcp_.*(?:use_figma|generate_figma_design)$/i.test(tool.name))
|
|
105
|
+
return true;
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
function unsupportedAcpUi(feature) {
|
|
109
|
+
throw new Error(`${feature} is not available in ACP mode yet. In Zed or other ACP clients, use argument-driven commands when available, or run this command in the terminal nanoPencil UI.`);
|
|
110
|
+
}
|
|
111
|
+
function formatMoney(value) {
|
|
112
|
+
return value.toFixed(4);
|
|
113
|
+
}
|
|
37
114
|
/**
|
|
38
115
|
* Create an extension UI context for ACP mode.
|
|
39
116
|
* Returns silent defaults since ACP mode has no interactive UI.
|
|
40
117
|
*/
|
|
41
118
|
function createAcpExtensionUIContext() {
|
|
42
119
|
return {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
120
|
+
__nonInteractive: true,
|
|
121
|
+
select: async () => unsupportedAcpUi("Interactive selection"),
|
|
122
|
+
confirm: async () => unsupportedAcpUi("Interactive confirmation"),
|
|
123
|
+
input: async () => unsupportedAcpUi("Interactive text input"),
|
|
124
|
+
editor: async () => unsupportedAcpUi("Interactive editor"),
|
|
47
125
|
notify(message, type) {
|
|
48
126
|
process.stderr.write(`[${type ?? "info"}] ${message}\n`);
|
|
49
127
|
},
|
|
@@ -58,7 +136,7 @@ function createAcpExtensionUIContext() {
|
|
|
58
136
|
setEditorText() { },
|
|
59
137
|
getEditorText: () => "",
|
|
60
138
|
async custom() {
|
|
61
|
-
return
|
|
139
|
+
return unsupportedAcpUi("Custom interactive UI");
|
|
62
140
|
},
|
|
63
141
|
onTerminalInput() {
|
|
64
142
|
return () => { };
|
|
@@ -90,58 +168,178 @@ function createAcpExtensionUIContext() {
|
|
|
90
168
|
class NanoPencilAgent {
|
|
91
169
|
connection;
|
|
92
170
|
session;
|
|
93
|
-
|
|
94
|
-
|
|
171
|
+
sessions = new Map();
|
|
172
|
+
currentSessionId;
|
|
173
|
+
createSessionForCwd;
|
|
174
|
+
extensionBindings;
|
|
175
|
+
ready;
|
|
176
|
+
constructor(connection, session, options = {}) {
|
|
95
177
|
this.connection = connection;
|
|
96
178
|
this.session = session;
|
|
97
|
-
this.
|
|
179
|
+
this.createSessionForCwd = options.createSessionForCwd;
|
|
180
|
+
this.extensionBindings = {
|
|
181
|
+
uiContext: createAcpExtensionUIContext(),
|
|
182
|
+
commandContextActions: {
|
|
183
|
+
waitForIdle: () => this.session.agent.waitForIdle(),
|
|
184
|
+
newSession: async (options) => {
|
|
185
|
+
const success = await this.session.newSession(options);
|
|
186
|
+
return { cancelled: !success };
|
|
187
|
+
},
|
|
188
|
+
fork: async (entryId) => {
|
|
189
|
+
const result = await this.session.fork(entryId);
|
|
190
|
+
return { cancelled: result.cancelled };
|
|
191
|
+
},
|
|
192
|
+
navigateTree: async (targetId, options) => {
|
|
193
|
+
const result = await this.session.navigateTree(targetId, options);
|
|
194
|
+
return { cancelled: result.cancelled };
|
|
195
|
+
},
|
|
196
|
+
switchSession: async (sessionPath) => {
|
|
197
|
+
const success = await this.session.switchSession(sessionPath);
|
|
198
|
+
return { cancelled: !success };
|
|
199
|
+
},
|
|
200
|
+
reload: async () => {
|
|
201
|
+
await this.session.reload();
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
shutdownHandler: () => {
|
|
205
|
+
process.exit(0);
|
|
206
|
+
},
|
|
207
|
+
onError: (err) => {
|
|
208
|
+
process.stderr.write(`[extension_error] ${err.extensionPath}: ${err.error}\n`);
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
this.ready = this.bindSession(this.session);
|
|
98
212
|
}
|
|
99
|
-
async initialize(
|
|
213
|
+
async initialize(_params) {
|
|
214
|
+
await this.ready;
|
|
100
215
|
return {
|
|
101
216
|
protocolVersion: acp.PROTOCOL_VERSION,
|
|
217
|
+
agentInfo: {
|
|
218
|
+
name: "nanoPencil",
|
|
219
|
+
version: "acp",
|
|
220
|
+
},
|
|
102
221
|
agentCapabilities: {
|
|
103
|
-
loadSession:
|
|
222
|
+
loadSession: true,
|
|
223
|
+
sessionCapabilities: {
|
|
224
|
+
list: {},
|
|
225
|
+
},
|
|
104
226
|
},
|
|
105
227
|
};
|
|
106
228
|
}
|
|
107
229
|
async newSession(params) {
|
|
108
|
-
|
|
109
|
-
this.
|
|
110
|
-
|
|
230
|
+
await this.ready;
|
|
231
|
+
await this.ensureWorkspaceSession(params.cwd);
|
|
232
|
+
await this.session.newSession();
|
|
233
|
+
const sessionId = this.session.sessionManager.getSessionId();
|
|
234
|
+
const state = this.createStateFromCurrentSession(sessionId, params.cwd);
|
|
235
|
+
this.sessions.set(sessionId, state);
|
|
236
|
+
this.currentSessionId = sessionId;
|
|
237
|
+
await this.applySessionMode(state);
|
|
238
|
+
await this.emitSessionMetadata(state);
|
|
239
|
+
await this.emitAvailableCommands(sessionId);
|
|
240
|
+
return {
|
|
241
|
+
sessionId,
|
|
242
|
+
models: this.buildModelState(),
|
|
243
|
+
modes: this.buildModeState(state.modeId),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
async loadSession(params) {
|
|
247
|
+
await this.ready;
|
|
248
|
+
const info = await this.findSessionInfo(params.sessionId, params.cwd);
|
|
249
|
+
if (!info) {
|
|
250
|
+
throw new Error(`Session ${params.sessionId} not found`);
|
|
251
|
+
}
|
|
252
|
+
await this.ensureWorkspaceSession(info.cwd || params.cwd);
|
|
253
|
+
const switched = await this.session.switchSession(info.path);
|
|
254
|
+
if (!switched) {
|
|
255
|
+
throw new Error(`Failed to load session ${params.sessionId}`);
|
|
256
|
+
}
|
|
257
|
+
const existing = this.sessions.get(params.sessionId);
|
|
258
|
+
const state = {
|
|
259
|
+
sessionId: params.sessionId,
|
|
260
|
+
sessionFile: info.path,
|
|
261
|
+
cwd: info.cwd || params.cwd,
|
|
262
|
+
title: info.name || info.firstMessage || existing?.title,
|
|
263
|
+
modeId: existing?.modeId ?? "ask",
|
|
264
|
+
abortController: null,
|
|
265
|
+
allowAllMutations: existing?.allowAllMutations ?? false,
|
|
266
|
+
rejectAllMutations: existing?.rejectAllMutations ?? false,
|
|
267
|
+
};
|
|
268
|
+
this.sessions.set(params.sessionId, state);
|
|
269
|
+
this.currentSessionId = params.sessionId;
|
|
270
|
+
await this.applySessionMode(state);
|
|
271
|
+
await this.emitSessionMetadata(state);
|
|
272
|
+
await this.emitAvailableCommands(params.sessionId);
|
|
273
|
+
await this.replayHistory(params.sessionId);
|
|
274
|
+
return {
|
|
275
|
+
models: this.buildModelState(),
|
|
276
|
+
modes: this.buildModeState(state.modeId),
|
|
277
|
+
};
|
|
111
278
|
}
|
|
112
|
-
async
|
|
113
|
-
|
|
279
|
+
async listSessions(params) {
|
|
280
|
+
await this.ready;
|
|
281
|
+
const infos = params.cwd
|
|
282
|
+
? await SessionManager.list(params.cwd)
|
|
283
|
+
: await SessionManager.listAll();
|
|
284
|
+
const merged = new Map();
|
|
285
|
+
for (const info of infos) {
|
|
286
|
+
merged.set(info.id, {
|
|
287
|
+
sessionId: info.id,
|
|
288
|
+
cwd: info.cwd,
|
|
289
|
+
title: info.name || info.firstMessage || undefined,
|
|
290
|
+
updatedAt: info.modified.toISOString(),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
for (const state of this.sessions.values()) {
|
|
294
|
+
if (params.cwd && state.cwd !== params.cwd)
|
|
295
|
+
continue;
|
|
296
|
+
if (!merged.has(state.sessionId)) {
|
|
297
|
+
merged.set(state.sessionId, {
|
|
298
|
+
sessionId: state.sessionId,
|
|
299
|
+
cwd: state.cwd,
|
|
300
|
+
title: state.title,
|
|
301
|
+
updatedAt: new Date().toISOString(),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
sessions: Array.from(merged.values()),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
async authenticate(_params) {
|
|
310
|
+
await this.ready;
|
|
114
311
|
return;
|
|
115
312
|
}
|
|
116
313
|
async prompt(params) {
|
|
117
|
-
|
|
118
|
-
const sessionState = this.
|
|
119
|
-
|
|
120
|
-
throw new Error(`Session ${sessionId} not found`);
|
|
121
|
-
}
|
|
122
|
-
// Abort any previous prompt
|
|
314
|
+
await this.ready;
|
|
315
|
+
const sessionState = this.requireSession(params.sessionId);
|
|
316
|
+
await this.activateSession(sessionState);
|
|
123
317
|
sessionState.abortController?.abort();
|
|
124
318
|
sessionState.abortController = new AbortController();
|
|
125
|
-
|
|
126
|
-
const userText = prompt
|
|
319
|
+
const userText = params.prompt
|
|
127
320
|
.filter((block) => "text" in block && typeof block.text === "string")
|
|
128
321
|
.map((block) => block.text)
|
|
129
322
|
.join("\n");
|
|
130
|
-
|
|
323
|
+
const builtinHandled = await this.handleBuiltinSlashCommand(params.sessionId, sessionState, userText);
|
|
324
|
+
if (builtinHandled) {
|
|
325
|
+
return { stopReason: "end_turn" };
|
|
326
|
+
}
|
|
131
327
|
const unsubscribe = this.session.subscribe((event) => {
|
|
132
|
-
this.mapEventToAcp(sessionId, event);
|
|
328
|
+
this.mapEventToAcp(params.sessionId, event);
|
|
133
329
|
});
|
|
134
330
|
try {
|
|
135
331
|
// @ts-expect-error - source is for internal use
|
|
136
332
|
await this.session.prompt(userText, { source: "acp" });
|
|
333
|
+
await this.emitSessionMetadata(sessionState);
|
|
137
334
|
return { stopReason: "end_turn" };
|
|
138
335
|
}
|
|
139
336
|
catch (error) {
|
|
140
337
|
if (sessionState.abortController.signal.aborted) {
|
|
141
338
|
return { stopReason: "cancelled" };
|
|
142
339
|
}
|
|
143
|
-
|
|
144
|
-
process.stderr.write(`[error] ${
|
|
340
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
341
|
+
process.stderr.write(`[error] ${message}\n`);
|
|
342
|
+
await this.sendAssistantText(params.sessionId, `Command failed: ${message}`);
|
|
145
343
|
return { stopReason: "end_turn" };
|
|
146
344
|
}
|
|
147
345
|
finally {
|
|
@@ -150,15 +348,440 @@ class NanoPencilAgent {
|
|
|
150
348
|
}
|
|
151
349
|
}
|
|
152
350
|
async cancel(params) {
|
|
153
|
-
|
|
351
|
+
await this.ready;
|
|
352
|
+
const sessionState = this.sessions.get(params.sessionId);
|
|
154
353
|
if (sessionState) {
|
|
354
|
+
await this.activateSession(sessionState);
|
|
155
355
|
sessionState.abortController?.abort();
|
|
156
356
|
await this.session.abort();
|
|
157
357
|
}
|
|
158
358
|
}
|
|
159
359
|
async setSessionMode(params) {
|
|
160
|
-
|
|
161
|
-
|
|
360
|
+
await this.ready;
|
|
361
|
+
const sessionState = this.requireSession(params.sessionId);
|
|
362
|
+
if (!ACP_MODES.some((mode) => mode.id === params.modeId)) {
|
|
363
|
+
throw new Error(`Unknown ACP mode: ${params.modeId}`);
|
|
364
|
+
}
|
|
365
|
+
sessionState.modeId = params.modeId;
|
|
366
|
+
sessionState.allowAllMutations = false;
|
|
367
|
+
sessionState.rejectAllMutations = false;
|
|
368
|
+
await this.activateSession(sessionState);
|
|
369
|
+
await this.connection.sessionUpdate({
|
|
370
|
+
sessionId: params.sessionId,
|
|
371
|
+
update: {
|
|
372
|
+
sessionUpdate: "current_mode_update",
|
|
373
|
+
currentModeId: sessionState.modeId,
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
return {};
|
|
377
|
+
}
|
|
378
|
+
async unstable_setSessionModel(params) {
|
|
379
|
+
await this.ready;
|
|
380
|
+
const sessionState = this.requireSession(params.sessionId);
|
|
381
|
+
await this.activateSession(sessionState);
|
|
382
|
+
const model = this.parseAcpModelId(params.modelId);
|
|
383
|
+
if (!model) {
|
|
384
|
+
throw new Error(`Unknown model: ${params.modelId}`);
|
|
385
|
+
}
|
|
386
|
+
await this.session.setModel(model);
|
|
387
|
+
await this.emitSessionMetadata(sessionState);
|
|
388
|
+
return {};
|
|
389
|
+
}
|
|
390
|
+
requireSession(sessionId) {
|
|
391
|
+
const state = this.sessions.get(sessionId);
|
|
392
|
+
if (!state) {
|
|
393
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
394
|
+
}
|
|
395
|
+
return state;
|
|
396
|
+
}
|
|
397
|
+
createStateFromCurrentSession(sessionId, cwd) {
|
|
398
|
+
const sessionFile = this.session.sessionManager.getSessionFile();
|
|
399
|
+
return {
|
|
400
|
+
sessionId,
|
|
401
|
+
sessionFile,
|
|
402
|
+
cwd,
|
|
403
|
+
title: this.getCurrentSessionTitle(),
|
|
404
|
+
modeId: "ask",
|
|
405
|
+
abortController: null,
|
|
406
|
+
allowAllMutations: false,
|
|
407
|
+
rejectAllMutations: false,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
getCurrentSessionTitle() {
|
|
411
|
+
return (this.session.sessionManager.getSessionName() ||
|
|
412
|
+
this.session.agent.state.messages.find((message) => message.role === "user")
|
|
413
|
+
? getMessageText(this.session.agent.state.messages.find((message) => message.role === "user")).slice(0, 80)
|
|
414
|
+
: undefined);
|
|
415
|
+
}
|
|
416
|
+
async findSessionInfo(sessionId, cwd) {
|
|
417
|
+
const existing = this.sessions.get(sessionId);
|
|
418
|
+
if (existing?.sessionFile) {
|
|
419
|
+
return {
|
|
420
|
+
id: existing.sessionId,
|
|
421
|
+
path: existing.sessionFile,
|
|
422
|
+
cwd: existing.cwd,
|
|
423
|
+
name: existing.title,
|
|
424
|
+
firstMessage: "",
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
const cwdSessions = await SessionManager.list(cwd);
|
|
428
|
+
const direct = cwdSessions.find((info) => info.id === sessionId);
|
|
429
|
+
if (direct)
|
|
430
|
+
return direct;
|
|
431
|
+
const allSessions = await SessionManager.listAll();
|
|
432
|
+
return allSessions.find((info) => info.id === sessionId);
|
|
433
|
+
}
|
|
434
|
+
async findSessionByQuery(query, cwd) {
|
|
435
|
+
const trimmed = query.trim().toLowerCase();
|
|
436
|
+
if (!trimmed)
|
|
437
|
+
return undefined;
|
|
438
|
+
const cwdSessions = await SessionManager.list(cwd);
|
|
439
|
+
const allSessions = await SessionManager.listAll();
|
|
440
|
+
const candidates = [...cwdSessions, ...allSessions].filter((info, index, array) => array.findIndex((other) => other.id === info.id) === index);
|
|
441
|
+
return candidates.find((info) => {
|
|
442
|
+
const title = (info.name || info.firstMessage || "").toLowerCase();
|
|
443
|
+
return (info.id.toLowerCase() === trimmed ||
|
|
444
|
+
info.id.toLowerCase().startsWith(trimmed) ||
|
|
445
|
+
title.includes(trimmed));
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
buildModeState(currentModeId) {
|
|
449
|
+
return {
|
|
450
|
+
availableModes: ACP_MODES,
|
|
451
|
+
currentModeId,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
buildModelState() {
|
|
455
|
+
const models = this.session.modelRegistry.getAvailable();
|
|
456
|
+
const current = this.session.model;
|
|
457
|
+
const availableModels = models.map((model) => ({
|
|
458
|
+
modelId: this.toAcpModelId(model),
|
|
459
|
+
name: model.name || `${model.provider}/${model.id}`,
|
|
460
|
+
description: `${model.provider} / ${model.id}`,
|
|
461
|
+
}));
|
|
462
|
+
return {
|
|
463
|
+
availableModels,
|
|
464
|
+
currentModelId: current ? this.toAcpModelId(current) : availableModels[0]?.modelId ?? "unknown/unknown",
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
toAcpModelId(model) {
|
|
468
|
+
return `${model.provider}/${model.id}`;
|
|
469
|
+
}
|
|
470
|
+
parseAcpModelId(modelId) {
|
|
471
|
+
const slashIndex = modelId.indexOf("/");
|
|
472
|
+
if (slashIndex === -1)
|
|
473
|
+
return undefined;
|
|
474
|
+
const provider = modelId.slice(0, slashIndex);
|
|
475
|
+
const id = modelId.slice(slashIndex + 1);
|
|
476
|
+
return this.session.modelRegistry.find(provider, id);
|
|
477
|
+
}
|
|
478
|
+
async emitAvailableCommands(sessionId) {
|
|
479
|
+
await this.connection.sessionUpdate({
|
|
480
|
+
sessionId,
|
|
481
|
+
update: {
|
|
482
|
+
sessionUpdate: "available_commands_update",
|
|
483
|
+
availableCommands: createSlashCommandsUpdate(this.session),
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
async emitSessionMetadata(state) {
|
|
488
|
+
state.title = this.getCurrentSessionTitle() ?? state.title;
|
|
489
|
+
await this.connection.sessionUpdate({
|
|
490
|
+
sessionId: state.sessionId,
|
|
491
|
+
update: {
|
|
492
|
+
sessionUpdate: "session_info_update",
|
|
493
|
+
title: state.title ?? null,
|
|
494
|
+
updatedAt: new Date().toISOString(),
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
async replayHistory(sessionId) {
|
|
499
|
+
for (const message of this.session.agent.state.messages) {
|
|
500
|
+
if (message.role !== "user" && message.role !== "assistant")
|
|
501
|
+
continue;
|
|
502
|
+
const text = getMessageText(message);
|
|
503
|
+
if (!text.trim())
|
|
504
|
+
continue;
|
|
505
|
+
await this.connection.sessionUpdate({
|
|
506
|
+
sessionId,
|
|
507
|
+
update: {
|
|
508
|
+
sessionUpdate: message.role === "user" ? "user_message_chunk" : "agent_message_chunk",
|
|
509
|
+
content: textToContent(text),
|
|
510
|
+
messageId: createMessageId(),
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
async activateSession(state) {
|
|
516
|
+
await this.ensureWorkspaceSession(state.cwd);
|
|
517
|
+
const currentFile = this.session.sessionManager.getSessionFile();
|
|
518
|
+
if (state.sessionFile && state.sessionFile !== currentFile) {
|
|
519
|
+
const switched = await this.session.switchSession(state.sessionFile);
|
|
520
|
+
if (!switched) {
|
|
521
|
+
throw new Error(`Failed to switch to session ${state.sessionId}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
this.currentSessionId = state.sessionId;
|
|
525
|
+
await this.applySessionMode(state);
|
|
526
|
+
}
|
|
527
|
+
async bindSession(session) {
|
|
528
|
+
await session.bindExtensions(this.extensionBindings);
|
|
529
|
+
}
|
|
530
|
+
async ensureWorkspaceSession(cwd) {
|
|
531
|
+
if (!cwd || this.session.cwd === cwd || !this.createSessionForCwd) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
await this.session.extensionRunner?.emit({ type: "session_shutdown" });
|
|
535
|
+
const nextSession = await this.createSessionForCwd(cwd);
|
|
536
|
+
this.session = nextSession;
|
|
537
|
+
await this.bindSession(nextSession);
|
|
538
|
+
}
|
|
539
|
+
async handleBuiltinSlashCommand(sessionId, state, text) {
|
|
540
|
+
const trimmed = text.trim();
|
|
541
|
+
if (!trimmed.startsWith("/"))
|
|
542
|
+
return false;
|
|
543
|
+
if (trimmed === "/new") {
|
|
544
|
+
await this.session.newSession();
|
|
545
|
+
state.sessionFile = this.session.sessionManager.getSessionFile();
|
|
546
|
+
state.title = this.getCurrentSessionTitle();
|
|
547
|
+
await this.emitSessionMetadata(state);
|
|
548
|
+
await this.sendAssistantText(sessionId, "Started a new session.");
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
if (trimmed === "/reload") {
|
|
552
|
+
await this.session.reload();
|
|
553
|
+
await this.activateSession(state);
|
|
554
|
+
await this.emitAvailableCommands(sessionId);
|
|
555
|
+
await this.sendAssistantText(sessionId, "Reloaded extensions, skills, prompts, and themes.");
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
if (trimmed === "/session") {
|
|
559
|
+
const stats = this.session.getSessionStats();
|
|
560
|
+
const context = this.session.getContextUsage();
|
|
561
|
+
const current = this.session.model;
|
|
562
|
+
const lines = [
|
|
563
|
+
`Session ID: ${stats.sessionId}`,
|
|
564
|
+
`Session name: ${this.session.sessionManager.getSessionName() || "(unnamed)"}`,
|
|
565
|
+
`Session file: ${stats.sessionFile ?? "(in-memory)"}`,
|
|
566
|
+
`Model: ${current ? `${current.provider}/${current.id}` : "none"}`,
|
|
567
|
+
`Thinking: ${this.session.thinkingLevel}`,
|
|
568
|
+
`Messages: ${stats.totalMessages} total (${stats.userMessages} user, ${stats.assistantMessages} assistant, ${stats.toolResults} tool results)`,
|
|
569
|
+
`Tool calls: ${stats.toolCalls}`,
|
|
570
|
+
];
|
|
571
|
+
if (context) {
|
|
572
|
+
lines.push(`Context: ${context.tokens ?? "unknown"} / ${context.contextWindow} tokens${context.percent != null ? ` (${context.percent.toFixed(1)}%)` : ""}`);
|
|
573
|
+
}
|
|
574
|
+
await this.sendAssistantText(sessionId, lines.join("\n"));
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
if (trimmed === "/usage") {
|
|
578
|
+
const stats = this.session.getSessionStats();
|
|
579
|
+
const lines = [
|
|
580
|
+
"Usage summary",
|
|
581
|
+
`Input tokens: ${stats.tokens.input}`,
|
|
582
|
+
`Output tokens: ${stats.tokens.output}`,
|
|
583
|
+
`Cache read: ${stats.tokens.cacheRead}`,
|
|
584
|
+
`Cache write: ${stats.tokens.cacheWrite}`,
|
|
585
|
+
`Total tokens: ${stats.tokens.total}`,
|
|
586
|
+
`Estimated cost: $${formatMoney(stats.cost)}`,
|
|
587
|
+
];
|
|
588
|
+
await this.sendAssistantText(sessionId, lines.join("\n"));
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
if (trimmed === "/name" || trimmed.startsWith("/name ")) {
|
|
592
|
+
const arg = trimmed.slice("/name".length).trim();
|
|
593
|
+
if (!arg) {
|
|
594
|
+
await this.sendAssistantText(sessionId, `Current session name: ${this.session.sessionManager.getSessionName() || "(unnamed)"}`);
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
this.session.sessionManager.appendSessionInfo(arg);
|
|
598
|
+
state.title = arg;
|
|
599
|
+
await this.emitSessionMetadata(state);
|
|
600
|
+
await this.sendAssistantText(sessionId, `Session name set to "${arg}".`);
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
if (trimmed === "/resume" || trimmed.startsWith("/resume ")) {
|
|
604
|
+
const arg = trimmed.slice("/resume".length).trim();
|
|
605
|
+
if (!arg) {
|
|
606
|
+
const sessions = await SessionManager.list(state.cwd);
|
|
607
|
+
const summary = sessions
|
|
608
|
+
.slice(0, 10)
|
|
609
|
+
.map((info) => `- ${info.id} | ${info.name || info.firstMessage || "(untitled)"}`)
|
|
610
|
+
.join("\n");
|
|
611
|
+
await this.sendAssistantText(sessionId, summary
|
|
612
|
+
? `Recent sessions for this workspace:\n${summary}\n\nUse /resume <session-id-or-title>.`
|
|
613
|
+
: "No saved sessions were found for this workspace.");
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
const target = await this.findSessionByQuery(arg, state.cwd);
|
|
617
|
+
if (!target) {
|
|
618
|
+
await this.sendAssistantText(sessionId, `No saved session matched "${arg}". Use /resume with no arguments to list recent sessions.`);
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
const switched = await this.session.switchSession(target.path);
|
|
622
|
+
if (!switched) {
|
|
623
|
+
await this.sendAssistantText(sessionId, `Failed to load session ${target.id}.`);
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
state.sessionFile = target.path;
|
|
627
|
+
state.cwd = target.cwd;
|
|
628
|
+
state.title = target.name || target.firstMessage || state.title;
|
|
629
|
+
await this.emitSessionMetadata(state);
|
|
630
|
+
await this.emitAvailableCommands(sessionId);
|
|
631
|
+
await this.sendAssistantText(sessionId, `Resumed session ${target.id}${state.title ? ` (${state.title})` : ""}. Future turns now use that saved conversation context.`);
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
if (trimmed === "/thinking" || trimmed.startsWith("/thinking ")) {
|
|
635
|
+
const arg = trimmed.slice("/thinking".length).trim().toLowerCase();
|
|
636
|
+
if (!arg) {
|
|
637
|
+
const levels = this.session.getAvailableThinkingLevels().join(", ");
|
|
638
|
+
await this.sendAssistantText(sessionId, `Current thinking level: ${this.session.thinkingLevel}\nAvailable levels: ${levels}`);
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
const levels = this.session.getAvailableThinkingLevels();
|
|
642
|
+
if (!levels.includes(arg)) {
|
|
643
|
+
await this.sendAssistantText(sessionId, `Unknown thinking level: ${arg}\nAvailable levels: ${levels.join(", ")}`);
|
|
644
|
+
return true;
|
|
645
|
+
}
|
|
646
|
+
this.session.setThinkingLevel(arg);
|
|
647
|
+
await this.sendAssistantText(sessionId, `Thinking level set to ${this.session.thinkingLevel}.`);
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
if (trimmed === "/model" || trimmed.startsWith("/model ")) {
|
|
651
|
+
const arg = trimmed.slice("/model".length).trim();
|
|
652
|
+
if (!arg) {
|
|
653
|
+
const current = this.session.model;
|
|
654
|
+
const summary = this.session.modelRegistry
|
|
655
|
+
.getAvailable()
|
|
656
|
+
.slice(0, 20)
|
|
657
|
+
.map((model) => `- ${model.provider}/${model.id}`)
|
|
658
|
+
.join("\n");
|
|
659
|
+
await this.sendAssistantText(sessionId, `Current model: ${current ? `${current.provider}/${current.id}` : "none"}\nAvailable models:\n${summary}`);
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
const model = this.findExactModelMatch(arg);
|
|
663
|
+
if (!model) {
|
|
664
|
+
const matches = this.session.modelRegistry
|
|
665
|
+
.getAvailable()
|
|
666
|
+
.filter((candidate) => {
|
|
667
|
+
const full = `${candidate.provider}/${candidate.id}`.toLowerCase();
|
|
668
|
+
return full.includes(arg.toLowerCase()) || candidate.id.toLowerCase().includes(arg.toLowerCase());
|
|
669
|
+
})
|
|
670
|
+
.slice(0, 10)
|
|
671
|
+
.map((candidate) => `- ${candidate.provider}/${candidate.id}`)
|
|
672
|
+
.join("\n");
|
|
673
|
+
await this.sendAssistantText(sessionId, matches
|
|
674
|
+
? `No exact model match for "${arg}". Closest matches:\n${matches}`
|
|
675
|
+
: `No model match for "${arg}".`);
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
await this.session.setModel(model);
|
|
679
|
+
await this.sendAssistantText(sessionId, `Model switched to ${model.provider}/${model.id}.`);
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
if (trimmed === "/compact" || trimmed.startsWith("/compact ")) {
|
|
683
|
+
const instructions = trimmed.slice("/compact".length).trim() || undefined;
|
|
684
|
+
const result = await this.session.compact(instructions);
|
|
685
|
+
await this.sendAssistantText(sessionId, `Compaction completed.\nFirst kept entry: ${result.firstKeptEntryId}\nTokens before: ${result.tokensBefore}`);
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
findExactModelMatch(searchTerm) {
|
|
691
|
+
const term = searchTerm.trim().toLowerCase();
|
|
692
|
+
if (!term)
|
|
693
|
+
return undefined;
|
|
694
|
+
let provider;
|
|
695
|
+
let modelId = term;
|
|
696
|
+
if (term.includes("/")) {
|
|
697
|
+
const [rawProvider, rawModelId] = term.split("/", 2);
|
|
698
|
+
provider = rawProvider?.trim();
|
|
699
|
+
modelId = rawModelId?.trim() ?? "";
|
|
700
|
+
}
|
|
701
|
+
if (!modelId)
|
|
702
|
+
return undefined;
|
|
703
|
+
return this.session.modelRegistry.getAvailable().find((model) => {
|
|
704
|
+
if (provider && model.provider.toLowerCase() !== provider)
|
|
705
|
+
return false;
|
|
706
|
+
return model.id.toLowerCase() === modelId;
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
async sendAssistantText(sessionId, text) {
|
|
710
|
+
await this.connection.sessionUpdate({
|
|
711
|
+
sessionId,
|
|
712
|
+
update: {
|
|
713
|
+
sessionUpdate: "agent_message_chunk",
|
|
714
|
+
content: textToContent(text),
|
|
715
|
+
messageId: createMessageId(),
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
async applySessionMode(state) {
|
|
720
|
+
const allToolNames = this.session.getAllTools().map((tool) => tool.name);
|
|
721
|
+
if (state.modeId === "read-only") {
|
|
722
|
+
const readOnlyToolNames = allToolNames.filter((name) => ["read", "grep", "find", "ls", "time"].includes(name));
|
|
723
|
+
this.session.setActiveToolsByName(readOnlyToolNames);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
this.session.setActiveToolsByName(allToolNames);
|
|
727
|
+
if (state.modeId === "ask") {
|
|
728
|
+
const wrapped = this.wrapToolsForAskMode(this.session.agent.state.tools, state);
|
|
729
|
+
this.session.agent.setTools(wrapped);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
wrapToolsForAskMode(tools, state) {
|
|
733
|
+
return tools.map((tool) => {
|
|
734
|
+
if (!isMutatingTool(tool))
|
|
735
|
+
return tool;
|
|
736
|
+
return {
|
|
737
|
+
...tool,
|
|
738
|
+
execute: async (toolCallId, params, signal, onUpdate) => {
|
|
739
|
+
await this.requestPermissionIfNeeded(state, tool, toolCallId, params);
|
|
740
|
+
return tool.execute(toolCallId, params, signal, onUpdate);
|
|
741
|
+
},
|
|
742
|
+
};
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
async requestPermissionIfNeeded(state, tool, toolCallId, params) {
|
|
746
|
+
if (state.allowAllMutations)
|
|
747
|
+
return;
|
|
748
|
+
if (state.rejectAllMutations) {
|
|
749
|
+
throw new Error("Permission denied for mutating tools in this session.");
|
|
750
|
+
}
|
|
751
|
+
const options = [
|
|
752
|
+
{ optionId: "allow_once", name: "Allow once", kind: "allow_once" },
|
|
753
|
+
{ optionId: "allow_always", name: "Always allow", kind: "allow_always" },
|
|
754
|
+
{ optionId: "reject_once", name: "Reject once", kind: "reject_once" },
|
|
755
|
+
{ optionId: "reject_always", name: "Always reject", kind: "reject_always" },
|
|
756
|
+
];
|
|
757
|
+
const response = await this.connection.requestPermission({
|
|
758
|
+
sessionId: state.sessionId,
|
|
759
|
+
options,
|
|
760
|
+
toolCall: {
|
|
761
|
+
toolCallId,
|
|
762
|
+
title: `Run ${tool.name}`,
|
|
763
|
+
kind: mapToolKind(tool.name),
|
|
764
|
+
status: "pending",
|
|
765
|
+
locations: [],
|
|
766
|
+
rawInput: params,
|
|
767
|
+
},
|
|
768
|
+
});
|
|
769
|
+
if (response.outcome.outcome === "cancelled") {
|
|
770
|
+
throw new Error("Permission request was cancelled.");
|
|
771
|
+
}
|
|
772
|
+
switch (response.outcome.optionId) {
|
|
773
|
+
case "allow_once":
|
|
774
|
+
return;
|
|
775
|
+
case "allow_always":
|
|
776
|
+
state.allowAllMutations = true;
|
|
777
|
+
return;
|
|
778
|
+
case "reject_always":
|
|
779
|
+
state.rejectAllMutations = true;
|
|
780
|
+
throw new Error("Permission denied for mutating tools in this session.");
|
|
781
|
+
case "reject_once":
|
|
782
|
+
default:
|
|
783
|
+
throw new Error("Permission denied for this tool call.");
|
|
784
|
+
}
|
|
162
785
|
}
|
|
163
786
|
/**
|
|
164
787
|
* Map nanoPencil AgentSessionEvent to ACP session/update notifications.
|
|
@@ -169,29 +792,30 @@ class NanoPencilAgent {
|
|
|
169
792
|
const sub = event.assistantMessageEvent;
|
|
170
793
|
switch (sub.type) {
|
|
171
794
|
case "text_delta":
|
|
172
|
-
this.connection.sessionUpdate({
|
|
795
|
+
void this.connection.sessionUpdate({
|
|
173
796
|
sessionId,
|
|
174
797
|
update: {
|
|
175
798
|
sessionUpdate: "agent_message_chunk",
|
|
176
|
-
content:
|
|
799
|
+
content: textToContent(sub.delta),
|
|
800
|
+
messageId: createMessageId(),
|
|
177
801
|
},
|
|
178
802
|
});
|
|
179
803
|
break;
|
|
180
804
|
case "thinking_delta":
|
|
181
|
-
this.connection.sessionUpdate({
|
|
805
|
+
void this.connection.sessionUpdate({
|
|
182
806
|
sessionId,
|
|
183
807
|
update: {
|
|
184
808
|
sessionUpdate: "agent_thought_chunk",
|
|
185
|
-
content:
|
|
809
|
+
content: textToContent(sub.delta),
|
|
810
|
+
messageId: createMessageId(),
|
|
186
811
|
},
|
|
187
812
|
});
|
|
188
813
|
break;
|
|
189
|
-
// toolcall_start, toolcall_end, etc. are handled by tool_execution_* events
|
|
190
814
|
}
|
|
191
815
|
break;
|
|
192
816
|
}
|
|
193
817
|
case "tool_execution_start":
|
|
194
|
-
this.connection.sessionUpdate({
|
|
818
|
+
void this.connection.sessionUpdate({
|
|
195
819
|
sessionId,
|
|
196
820
|
update: {
|
|
197
821
|
sessionUpdate: "tool_call",
|
|
@@ -200,11 +824,12 @@ class NanoPencilAgent {
|
|
|
200
824
|
kind: mapToolKind(event.toolName),
|
|
201
825
|
status: "pending",
|
|
202
826
|
locations: [],
|
|
827
|
+
rawInput: event.args,
|
|
203
828
|
},
|
|
204
829
|
});
|
|
205
830
|
break;
|
|
206
831
|
case "tool_execution_end":
|
|
207
|
-
this.connection.sessionUpdate({
|
|
832
|
+
void this.connection.sessionUpdate({
|
|
208
833
|
sessionId,
|
|
209
834
|
update: {
|
|
210
835
|
sessionUpdate: "tool_call_update",
|
|
@@ -215,16 +840,14 @@ class NanoPencilAgent {
|
|
|
215
840
|
type: "content",
|
|
216
841
|
content: {
|
|
217
842
|
type: "text",
|
|
218
|
-
text:
|
|
219
|
-
? event.result
|
|
220
|
-
: JSON.stringify(event.result, null, 2),
|
|
843
|
+
text: asText(event.result),
|
|
221
844
|
},
|
|
222
845
|
},
|
|
223
846
|
],
|
|
847
|
+
rawOutput: event.result,
|
|
224
848
|
},
|
|
225
849
|
});
|
|
226
850
|
break;
|
|
227
|
-
// agent_start, agent_end, turn_start, turn_end, etc. don't need mapping
|
|
228
851
|
}
|
|
229
852
|
}
|
|
230
853
|
}
|
|
@@ -232,44 +855,12 @@ class NanoPencilAgent {
|
|
|
232
855
|
* Run in ACP mode.
|
|
233
856
|
* Listens for JSON-RPC 2.0 messages on stdin, outputs JSON-RPC responses/events on stdout.
|
|
234
857
|
*/
|
|
235
|
-
export async function runAcpMode(session) {
|
|
236
|
-
// Bind extensions with headless UI context
|
|
237
|
-
await session.bindExtensions({
|
|
238
|
-
uiContext: createAcpExtensionUIContext(),
|
|
239
|
-
commandContextActions: {
|
|
240
|
-
waitForIdle: () => session.agent.waitForIdle(),
|
|
241
|
-
newSession: async (options) => {
|
|
242
|
-
const success = await session.newSession(options);
|
|
243
|
-
return { cancelled: !success };
|
|
244
|
-
},
|
|
245
|
-
fork: async (entryId) => {
|
|
246
|
-
const result = await session.fork(entryId);
|
|
247
|
-
return { cancelled: result.cancelled };
|
|
248
|
-
},
|
|
249
|
-
navigateTree: async (targetId, options) => {
|
|
250
|
-
const result = await session.navigateTree(targetId, options);
|
|
251
|
-
return { cancelled: result.cancelled };
|
|
252
|
-
},
|
|
253
|
-
switchSession: async (sessionPath) => {
|
|
254
|
-
const success = await session.switchSession(sessionPath);
|
|
255
|
-
return { cancelled: !success };
|
|
256
|
-
},
|
|
257
|
-
reload: async () => {
|
|
258
|
-
await session.reload();
|
|
259
|
-
},
|
|
260
|
-
},
|
|
261
|
-
shutdownHandler: () => {
|
|
262
|
-
process.exit(0);
|
|
263
|
-
},
|
|
264
|
-
onError: (err) => {
|
|
265
|
-
process.stderr.write(`[extension_error] ${err.extensionPath}: ${err.error}\n`);
|
|
266
|
-
},
|
|
267
|
-
});
|
|
858
|
+
export async function runAcpMode(session, options = {}) {
|
|
268
859
|
// Set up ACP connection via stdin/stdout
|
|
269
860
|
const input = Writable.toWeb(process.stdout);
|
|
270
861
|
const output = Readable.toWeb(process.stdin);
|
|
271
862
|
const stream = acp.ndJsonStream(input, output);
|
|
272
|
-
new acp.AgentSideConnection((conn) => new NanoPencilAgent(conn, session), stream);
|
|
863
|
+
new acp.AgentSideConnection((conn) => new NanoPencilAgent(conn, session, options), stream);
|
|
273
864
|
// Keep process alive
|
|
274
865
|
return new Promise(() => { });
|
|
275
866
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pencil-agent/nano-pencil",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.15",
|
|
4
4
|
"description": "CLI writing agent with read, bash, edit, write tools and session management. Based on pi; supports DashScope Coding Plan. Soul enabled by default for AI personality evolution.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|