@pencil-agent/nano-pencil 1.11.32 → 1.11.33
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/model-registry.d.ts +9 -0
- package/dist/core/model-registry.js +50 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +4 -2
- package/dist/modes/interactive/components/custom-editor.js +33 -7
- package/dist/modes/interactive/components/index.d.ts +1 -0
- package/dist/modes/interactive/components/index.js +1 -0
- package/dist/modes/interactive/components/model-selector.d.ts +3 -1
- package/dist/modes/interactive/components/model-selector.js +12 -2
- package/dist/modes/interactive/interactive-mode.d.ts +16 -1
- package/dist/modes/interactive/interactive-mode.js +289 -27
- package/dist/nanopencil-defaults.d.ts +6 -0
- package/dist/nanopencil-defaults.js +59 -0
- package/package.json +2 -1
|
@@ -83,6 +83,15 @@ export declare class ModelRegistry {
|
|
|
83
83
|
*/
|
|
84
84
|
registerProvider(providerName: string, config: ProviderConfigInput): void;
|
|
85
85
|
private applyProviderConfig;
|
|
86
|
+
private static readonly OPENROUTER_JSON_BASE;
|
|
87
|
+
private static readonly OPENROUTER_JSON_API;
|
|
88
|
+
/**
|
|
89
|
+
* Append an OpenRouter model to models.json by id (same string as on openrouter.ai, e.g. x-ai/grok-4.20).
|
|
90
|
+
* API key is not written; use /login openrouter or OPENROUTER_API_KEY.
|
|
91
|
+
*/
|
|
92
|
+
appendOpenRouterModel(modelId: string, options?: {
|
|
93
|
+
name?: string;
|
|
94
|
+
}): void;
|
|
86
95
|
}
|
|
87
96
|
/**
|
|
88
97
|
* Input type for registerProvider API.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { getModels, getProviders, registerApiProvider, registerOAuthProvider, } from "@pencil-agent/ai";
|
|
8
8
|
import { Type } from "@sinclair/typebox";
|
|
9
9
|
import AjvModule from "ajv";
|
|
10
|
-
import { existsSync, readFileSync } from "fs";
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
11
11
|
import { join } from "path";
|
|
12
12
|
import { getAgentDir } from "../config.js";
|
|
13
13
|
import { clearConfigValueCache, resolveConfigValue, resolveHeaders } from "./config/resolve-config-value.js";
|
|
@@ -536,5 +536,54 @@ export class ModelRegistry {
|
|
|
536
536
|
});
|
|
537
537
|
}
|
|
538
538
|
}
|
|
539
|
+
static OPENROUTER_JSON_BASE = "https://openrouter.ai/api/v1";
|
|
540
|
+
static OPENROUTER_JSON_API = "openai-completions";
|
|
541
|
+
/**
|
|
542
|
+
* Append an OpenRouter model to models.json by id (same string as on openrouter.ai, e.g. x-ai/grok-4.20).
|
|
543
|
+
* API key is not written; use /login openrouter or OPENROUTER_API_KEY.
|
|
544
|
+
*/
|
|
545
|
+
appendOpenRouterModel(modelId, options) {
|
|
546
|
+
const providerKey = "openrouter";
|
|
547
|
+
const trimmed = modelId.trim();
|
|
548
|
+
if (!trimmed) {
|
|
549
|
+
throw new Error("OpenRouter model id cannot be empty");
|
|
550
|
+
}
|
|
551
|
+
const modelsPath = this.modelsJsonPath;
|
|
552
|
+
if (!modelsPath) {
|
|
553
|
+
throw new Error("models.json path is not configured");
|
|
554
|
+
}
|
|
555
|
+
let data;
|
|
556
|
+
if (existsSync(modelsPath)) {
|
|
557
|
+
const raw = readFileSync(modelsPath, "utf-8");
|
|
558
|
+
data = JSON.parse(raw);
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
data = { providers: {} };
|
|
562
|
+
}
|
|
563
|
+
if (!data.providers || typeof data.providers !== "object") {
|
|
564
|
+
data.providers = {};
|
|
565
|
+
}
|
|
566
|
+
const existing = data.providers[providerKey] ?? {};
|
|
567
|
+
const prevModels = Array.isArray(existing.models) ? [...existing.models] : [];
|
|
568
|
+
if (prevModels.some((m) => m && typeof m === "object" && m.id === trimmed)) {
|
|
569
|
+
throw new Error(`OpenRouter model "${trimmed}" already exists in models.json`);
|
|
570
|
+
}
|
|
571
|
+
const displayName = options?.name?.trim() || trimmed;
|
|
572
|
+
prevModels.push({
|
|
573
|
+
id: trimmed,
|
|
574
|
+
name: displayName,
|
|
575
|
+
input: ["text"],
|
|
576
|
+
contextWindow: 256000,
|
|
577
|
+
maxTokens: 8192,
|
|
578
|
+
});
|
|
579
|
+
data.providers[providerKey] = {
|
|
580
|
+
...existing,
|
|
581
|
+
baseUrl: existing.baseUrl ?? ModelRegistry.OPENROUTER_JSON_BASE,
|
|
582
|
+
api: existing.api ?? ModelRegistry.OPENROUTER_JSON_API,
|
|
583
|
+
models: prevModels,
|
|
584
|
+
};
|
|
585
|
+
writeFileSync(modelsPath, JSON.stringify(data, null, 2), "utf-8");
|
|
586
|
+
this.refresh();
|
|
587
|
+
}
|
|
539
588
|
}
|
|
540
589
|
//# sourceMappingURL=model-registry.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* [UPSTREAM]:
|
|
3
|
-
* [SURFACE]:
|
|
2
|
+
* [UPSTREAM]:
|
|
3
|
+
* [SURFACE]:
|
|
4
4
|
* [LOCUS]: modes/interactive/components/custom-editor.ts -
|
|
5
5
|
* [COVENANT]: Change → update this header
|
|
6
6
|
*/
|
|
@@ -17,6 +17,8 @@ export declare class CustomEditor extends Editor {
|
|
|
17
17
|
onPasteImage?: () => void;
|
|
18
18
|
/** Handler for extension-registered shortcuts. Returns true if handled. */
|
|
19
19
|
onExtensionShortcut?: (data: string) => boolean;
|
|
20
|
+
/** Handler for attachment navigation (arrow keys, delete). Returns true if handled. */
|
|
21
|
+
onAttachmentKey?: (data: string) => boolean;
|
|
20
22
|
constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager, options?: EditorOptions);
|
|
21
23
|
/**
|
|
22
24
|
* Register a handler for an app action.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* [UPSTREAM]:
|
|
3
|
-
* [SURFACE]:
|
|
2
|
+
* [UPSTREAM]:
|
|
3
|
+
* [SURFACE]:
|
|
4
4
|
* [LOCUS]: modes/interactive/components/custom-editor.ts -
|
|
5
5
|
* [COVENANT]: Change → update this header
|
|
6
6
|
*/
|
|
7
|
-
import { Editor } from "@pencil-agent/tui";
|
|
7
|
+
import { Editor, getEditorKeybindings, matchesKey } from "@pencil-agent/tui";
|
|
8
8
|
/**
|
|
9
9
|
* Custom editor that handles app-level keybindings for coding-agent.
|
|
10
10
|
*/
|
|
@@ -17,6 +17,8 @@ export class CustomEditor extends Editor {
|
|
|
17
17
|
onPasteImage;
|
|
18
18
|
/** Handler for extension-registered shortcuts. Returns true if handled. */
|
|
19
19
|
onExtensionShortcut;
|
|
20
|
+
/** Handler for attachment navigation (arrow keys, delete). Returns true if handled. */
|
|
21
|
+
onAttachmentKey;
|
|
20
22
|
constructor(tui, theme, keybindings, options) {
|
|
21
23
|
super(tui, theme, options);
|
|
22
24
|
this.keybindings = keybindings;
|
|
@@ -32,10 +34,19 @@ export class CustomEditor extends Editor {
|
|
|
32
34
|
if (this.onExtensionShortcut?.(data)) {
|
|
33
35
|
return;
|
|
34
36
|
}
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Detect image paste via empty bracketed paste — the terminal sends an
|
|
38
|
+
// empty bracket sequence when the clipboard contains an image (since
|
|
39
|
+
// raw image bytes can't be pasted as text). This is the ONLY trigger
|
|
40
|
+
// for clipboard image reading; we intentionally do NOT intercept the
|
|
41
|
+
// raw Ctrl+V key code because Windows keeps stale image data in the
|
|
42
|
+
// clipboard even after the user copies text, which would cause the
|
|
43
|
+
// wrong content to be pasted.
|
|
44
|
+
if (this.onPasteImage && data.includes("\x1b[200~") && data.includes("\x1b[201~")) {
|
|
45
|
+
const content = data.replace("\x1b[200~", "").replace("\x1b[201~", "").trim();
|
|
46
|
+
if (content.length === 0) {
|
|
47
|
+
this.onPasteImage();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
39
50
|
}
|
|
40
51
|
// Check app keybindings first
|
|
41
52
|
// Escape/interrupt - only if autocomplete is NOT active
|
|
@@ -69,6 +80,21 @@ export class CustomEditor extends Editor {
|
|
|
69
80
|
return;
|
|
70
81
|
}
|
|
71
82
|
}
|
|
83
|
+
// Forward navigation/delete keys to the attachment handler. The handler
|
|
84
|
+
// only consumes them when appropriate (e.g. Delete only when an
|
|
85
|
+
// attachment is selected), otherwise returns false so the editor
|
|
86
|
+
// processes the key normally.
|
|
87
|
+
if (this.onAttachmentKey) {
|
|
88
|
+
const kb = getEditorKeybindings();
|
|
89
|
+
if (kb.matches(data, "cursorUp") ||
|
|
90
|
+
kb.matches(data, "cursorDown") ||
|
|
91
|
+
matchesKey(data, "delete") ||
|
|
92
|
+
matchesKey(data, "backspace")) {
|
|
93
|
+
if (this.onAttachmentKey(data)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
72
98
|
// Pass to parent for editor handling
|
|
73
99
|
super.handleInput(data);
|
|
74
100
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* [COVENANT]: Change components → update this header
|
|
6
6
|
*/
|
|
7
7
|
export { ArminComponent } from "./armin.js";
|
|
8
|
+
export { AttachmentsBarComponent } from "./attachments-bar.js";
|
|
8
9
|
export { AssistantMessageComponent } from "./assistant-message.js";
|
|
9
10
|
export { promptForApiKey } from "./apikey-input.js";
|
|
10
11
|
export { BashExecutionComponent } from "./bash-execution.js";
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
// UI Components for extensions
|
|
8
8
|
export { ArminComponent } from "./armin.js";
|
|
9
|
+
export { AttachmentsBarComponent } from "./attachments-bar.js";
|
|
9
10
|
export { AssistantMessageComponent } from "./assistant-message.js";
|
|
10
11
|
export { promptForApiKey } from "./apikey-input.js";
|
|
11
12
|
export { BashExecutionComponent } from "./bash-execution.js";
|
|
@@ -35,9 +35,11 @@ export declare class ModelSelectorComponent extends Container implements Focusab
|
|
|
35
35
|
private scopeText?;
|
|
36
36
|
private scopeHintText?;
|
|
37
37
|
private filterByProvider?;
|
|
38
|
+
/** When set, Ctrl+N runs this (parent closes selector and prompts for OpenRouter model id). */
|
|
39
|
+
private onAddOpenRouterModel?;
|
|
38
40
|
get focused(): boolean;
|
|
39
41
|
set focused(value: boolean);
|
|
40
|
-
constructor(tui: TUI, currentModel: Model<any> | undefined, settingsManager: SettingsManager, modelRegistry: ModelRegistry, scopedModels: ReadonlyArray<ScopedModelItem>, ensureProviderConfigured: EnsureProviderConfigured | undefined, onSelect: (model: Model<any>) => void, onCancel: () => void, initialSearchInput?: string, filterByProvider?: string);
|
|
42
|
+
constructor(tui: TUI, currentModel: Model<any> | undefined, settingsManager: SettingsManager, modelRegistry: ModelRegistry, scopedModels: ReadonlyArray<ScopedModelItem>, ensureProviderConfigured: EnsureProviderConfigured | undefined, onSelect: (model: Model<any>) => void, onCancel: () => void, initialSearchInput?: string, filterByProvider?: string, onAddOpenRouterModel?: () => void);
|
|
41
43
|
private loadModels;
|
|
42
44
|
private sortModels;
|
|
43
45
|
private getScopeText;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* [COVENANT]: Change → update this header
|
|
6
6
|
*/
|
|
7
7
|
import { modelsAreEqual } from "@pencil-agent/ai";
|
|
8
|
-
import { Container, fuzzyFilter, getEditorKeybindings, Input, Spacer, Text, } from "@pencil-agent/tui";
|
|
8
|
+
import { Container, fuzzyFilter, getEditorKeybindings, Input, matchesKey, Spacer, Text, } from "@pencil-agent/tui";
|
|
9
9
|
import { theme } from "../theme/theme.js";
|
|
10
10
|
import { promptForApiKey } from "./apikey-input.js";
|
|
11
11
|
import { DynamicBorder } from "./dynamic-border.js";
|
|
@@ -32,6 +32,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
32
32
|
scopeText;
|
|
33
33
|
scopeHintText;
|
|
34
34
|
filterByProvider;
|
|
35
|
+
/** When set, Ctrl+N runs this (parent closes selector and prompts for OpenRouter model id). */
|
|
36
|
+
onAddOpenRouterModel;
|
|
35
37
|
get focused() {
|
|
36
38
|
return this._focused;
|
|
37
39
|
}
|
|
@@ -39,7 +41,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
39
41
|
this._focused = value;
|
|
40
42
|
this.searchInput.focused = value;
|
|
41
43
|
}
|
|
42
|
-
constructor(tui, currentModel, settingsManager, modelRegistry, scopedModels, ensureProviderConfigured, onSelect, onCancel, initialSearchInput, filterByProvider) {
|
|
44
|
+
constructor(tui, currentModel, settingsManager, modelRegistry, scopedModels, ensureProviderConfigured, onSelect, onCancel, initialSearchInput, filterByProvider, onAddOpenRouterModel) {
|
|
43
45
|
super();
|
|
44
46
|
this.tui = tui;
|
|
45
47
|
this.currentModel = currentModel;
|
|
@@ -48,6 +50,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
48
50
|
this.scopedModels = scopedModels;
|
|
49
51
|
this.ensureProviderConfigured = ensureProviderConfigured;
|
|
50
52
|
this.filterByProvider = filterByProvider;
|
|
53
|
+
this.onAddOpenRouterModel = onAddOpenRouterModel;
|
|
51
54
|
this.scope = scopedModels.length > 0 && !filterByProvider ? "scoped" : "all";
|
|
52
55
|
this.onSelectCallback = onSelect;
|
|
53
56
|
this.onCancelCallback = onCancel;
|
|
@@ -73,6 +76,10 @@ export class ModelSelectorComponent extends Container {
|
|
|
73
76
|
}
|
|
74
77
|
};
|
|
75
78
|
this.addChild(this.searchInput);
|
|
79
|
+
if (this.onAddOpenRouterModel) {
|
|
80
|
+
this.addChild(new Spacer(1));
|
|
81
|
+
this.addChild(new Text(theme.fg("muted", "Ctrl+N: add OpenRouter model by id (same as openrouter.ai)"), 0, 0));
|
|
82
|
+
}
|
|
76
83
|
this.addChild(new Spacer(1));
|
|
77
84
|
this.listContainer = new Container();
|
|
78
85
|
this.addChild(this.listContainer);
|
|
@@ -255,6 +262,9 @@ export class ModelSelectorComponent extends Container {
|
|
|
255
262
|
else if (kb.matches(keyData, "selectCancel")) {
|
|
256
263
|
this.onCancelCallback();
|
|
257
264
|
}
|
|
265
|
+
else if (this.onAddOpenRouterModel && matchesKey(keyData, "ctrl+n")) {
|
|
266
|
+
this.onAddOpenRouterModel();
|
|
267
|
+
}
|
|
258
268
|
else {
|
|
259
269
|
this.searchInput.handleInput(keyData);
|
|
260
270
|
this.filterModels(this.searchInput.getValue());
|
|
@@ -19,6 +19,8 @@ export interface InteractiveModeOptions {
|
|
|
19
19
|
}
|
|
20
20
|
export declare class InteractiveMode {
|
|
21
21
|
private options;
|
|
22
|
+
private static clipboardImageSeq;
|
|
23
|
+
private clipboardImageFiles;
|
|
22
24
|
private session;
|
|
23
25
|
private ui;
|
|
24
26
|
private chatContainer;
|
|
@@ -71,6 +73,10 @@ export declare class InteractiveMode {
|
|
|
71
73
|
private headerContainer;
|
|
72
74
|
private builtInHeader;
|
|
73
75
|
private customHeader;
|
|
76
|
+
private attachments;
|
|
77
|
+
private selectedAttachmentIndex;
|
|
78
|
+
private attachmentsContainer;
|
|
79
|
+
private attachmentsBar;
|
|
74
80
|
private get agent();
|
|
75
81
|
private get sessionManager();
|
|
76
82
|
private get settingsManager();
|
|
@@ -194,7 +200,6 @@ export declare class InteractiveMode {
|
|
|
194
200
|
* Show a notification for extensions.
|
|
195
201
|
*/
|
|
196
202
|
private showExtensionNotify;
|
|
197
|
-
private isMemoryTraceMessage;
|
|
198
203
|
private shouldRenderToolTrace;
|
|
199
204
|
private hasActiveExtensionPrompt;
|
|
200
205
|
private restoreEditorFocusIfPossible;
|
|
@@ -210,12 +215,22 @@ export declare class InteractiveMode {
|
|
|
210
215
|
private showExtensionError;
|
|
211
216
|
private setupKeyHandlers;
|
|
212
217
|
private handleClipboardImagePaste;
|
|
218
|
+
private updateAttachmentsBar;
|
|
219
|
+
private deleteAttachment;
|
|
220
|
+
private handleAttachmentKeyNavigation;
|
|
221
|
+
/**
|
|
222
|
+
* Convert attachment files to ImageContent array for sending to the model.
|
|
223
|
+
* Reads each file, base64-encodes, and resizes.
|
|
224
|
+
*/
|
|
225
|
+
private processAttachmentFiles;
|
|
213
226
|
/**
|
|
214
227
|
* Extract image file paths from text, read them as base64 ImageContent,
|
|
215
228
|
* and return the cleaned text with image references plus the image array.
|
|
216
229
|
*/
|
|
217
230
|
private extractImagesFromText;
|
|
218
231
|
private setupEditorSubmitHandler;
|
|
232
|
+
private cleanupStaleClipboardFiles;
|
|
233
|
+
private cleanupClipboardImages;
|
|
219
234
|
private subscribeToAgent;
|
|
220
235
|
private addSessionNavigationBanner;
|
|
221
236
|
private handleEvent;
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* [UPSTREAM]: Depends on agent-core, ai, tui, core/* (session, model, config, tools)
|
|
3
|
-
* [SURFACE]: InteractiveMode class, runInteractiveMode()
|
|
4
|
-
* [LOCUS]: modes/interactive/interactive-mode.ts - TUI orchestration hub
|
|
5
|
-
* [COVENANT]: Change TUI behavior → update P2 modes/CLAUDE.md
|
|
6
|
-
*/
|
|
7
|
-
import * as crypto from "node:crypto";
|
|
8
1
|
import * as fs from "node:fs";
|
|
9
2
|
import * as os from "node:os";
|
|
10
3
|
import * as path from "node:path";
|
|
@@ -31,6 +24,7 @@ import { ensureTool } from "../../core/utils/tools-manager.js";
|
|
|
31
24
|
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
|
|
32
25
|
import { formatDimensionNote, resizeImage } from "../utils/image-resize.js";
|
|
33
26
|
import { ArminComponent } from "./components/armin.js";
|
|
27
|
+
import { AttachmentsBarComponent } from "./components/attachments-bar.js";
|
|
34
28
|
import { AssistantMessageComponent } from "./components/assistant-message.js";
|
|
35
29
|
import { BashExecutionComponent } from "./components/bash-execution.js";
|
|
36
30
|
import { BorderedLoader } from "./components/bordered-loader.js";
|
|
@@ -68,6 +62,8 @@ function isExpandable(obj) {
|
|
|
68
62
|
}
|
|
69
63
|
export class InteractiveMode {
|
|
70
64
|
options;
|
|
65
|
+
static clipboardImageSeq = 0;
|
|
66
|
+
clipboardImageFiles = [];
|
|
71
67
|
session;
|
|
72
68
|
ui;
|
|
73
69
|
chatContainer;
|
|
@@ -141,6 +137,11 @@ export class InteractiveMode {
|
|
|
141
137
|
builtInHeader = undefined;
|
|
142
138
|
// Custom header from extension (undefined = use built-in header)
|
|
143
139
|
customHeader = undefined;
|
|
140
|
+
// Attachments state
|
|
141
|
+
attachments = [];
|
|
142
|
+
selectedAttachmentIndex = -1;
|
|
143
|
+
attachmentsContainer = undefined;
|
|
144
|
+
attachmentsBar = undefined;
|
|
144
145
|
// Convenience accessors
|
|
145
146
|
get agent() {
|
|
146
147
|
return this.session.agent;
|
|
@@ -172,6 +173,8 @@ export class InteractiveMode {
|
|
|
172
173
|
});
|
|
173
174
|
this.editor = this.defaultEditor;
|
|
174
175
|
this.editorContainer = new Container();
|
|
176
|
+
this.attachmentsContainer = new Container();
|
|
177
|
+
this.editorContainer.addChild(this.attachmentsContainer);
|
|
175
178
|
this.editorContainer.addChild(this.editor);
|
|
176
179
|
this.footerDataProvider = new FooterDataProvider(session.cwd);
|
|
177
180
|
this.footer = new FooterComponent(session, this.footerDataProvider, this.settingsManager.getShowTokenStats());
|
|
@@ -255,6 +258,8 @@ export class InteractiveMode {
|
|
|
255
258
|
async init() {
|
|
256
259
|
if (this.isInitialized)
|
|
257
260
|
return;
|
|
261
|
+
// Clean up stale clipboard image files from previous sessions
|
|
262
|
+
this.cleanupStaleClipboardFiles();
|
|
258
263
|
// Do not show changelog on startup; version check will prompt to update CLI when newer version exists
|
|
259
264
|
// Ensure fd and rg are available (downloads if missing, adds to PATH via getBinDir)
|
|
260
265
|
// Both are needed: fd for autocomplete, rg for grep tool and bash commands
|
|
@@ -1345,11 +1350,6 @@ export class InteractiveMode {
|
|
|
1345
1350
|
* Show a notification for extensions.
|
|
1346
1351
|
*/
|
|
1347
1352
|
showExtensionNotify(message, type) {
|
|
1348
|
-
if (type !== "error" &&
|
|
1349
|
-
this.isMemoryTraceMessage(message) &&
|
|
1350
|
-
!this.settingsManager.getShowMemoryTrace()) {
|
|
1351
|
-
return;
|
|
1352
|
-
}
|
|
1353
1353
|
if (type === "error") {
|
|
1354
1354
|
this.showError(message);
|
|
1355
1355
|
}
|
|
@@ -1360,9 +1360,6 @@ export class InteractiveMode {
|
|
|
1360
1360
|
this.showStatus(message);
|
|
1361
1361
|
}
|
|
1362
1362
|
}
|
|
1363
|
-
isMemoryTraceMessage(message) {
|
|
1364
|
-
return message.startsWith("NanoMem");
|
|
1365
|
-
}
|
|
1366
1363
|
shouldRenderToolTrace(toolName) {
|
|
1367
1364
|
if (toolName.startsWith("nanomem_")) {
|
|
1368
1365
|
return this.settingsManager.getShowMemoryTrace();
|
|
@@ -1544,6 +1541,9 @@ export class InteractiveMode {
|
|
|
1544
1541
|
if (this.loadingAnimation) {
|
|
1545
1542
|
this.restoreQueuedMessagesToEditor({ abort: true });
|
|
1546
1543
|
}
|
|
1544
|
+
else if (this.session.isStreaming) {
|
|
1545
|
+
this.agent.abort();
|
|
1546
|
+
}
|
|
1547
1547
|
else if (this.session.isBashRunning) {
|
|
1548
1548
|
this.session.abortBash();
|
|
1549
1549
|
}
|
|
@@ -1604,6 +1604,10 @@ export class InteractiveMode {
|
|
|
1604
1604
|
this.defaultEditor.onPasteImage = () => {
|
|
1605
1605
|
this.handleClipboardImagePaste();
|
|
1606
1606
|
};
|
|
1607
|
+
// Handle attachment navigation keys (arrow keys, delete)
|
|
1608
|
+
this.defaultEditor.onAttachmentKey = (data) => {
|
|
1609
|
+
return this.handleAttachmentKeyNavigation(data);
|
|
1610
|
+
};
|
|
1607
1611
|
}
|
|
1608
1612
|
async handleClipboardImagePaste() {
|
|
1609
1613
|
try {
|
|
@@ -1611,20 +1615,138 @@ export class InteractiveMode {
|
|
|
1611
1615
|
if (!image) {
|
|
1612
1616
|
return;
|
|
1613
1617
|
}
|
|
1614
|
-
//
|
|
1615
|
-
|
|
1618
|
+
// Save to a dedicated _paste/ directory so the model finds ONLY
|
|
1619
|
+
// the clipboard image when browsing (avoids confusion with other
|
|
1620
|
+
// image files like IMG_*.JPG in the project root).
|
|
1616
1621
|
const ext = extensionForImageMimeType(image.mimeType) ?? "png";
|
|
1617
|
-
const
|
|
1618
|
-
const
|
|
1622
|
+
const seq = ++InteractiveMode.clipboardImageSeq;
|
|
1623
|
+
const fileName = `image_${seq}.${ext}`;
|
|
1624
|
+
const pasteDir = path.join(this.session.cwd, "_paste");
|
|
1625
|
+
fs.mkdirSync(pasteDir, { recursive: true });
|
|
1626
|
+
const filePath = path.join(pasteDir, fileName);
|
|
1619
1627
|
fs.writeFileSync(filePath, Buffer.from(image.bytes));
|
|
1620
|
-
//
|
|
1621
|
-
this.
|
|
1628
|
+
// Track for cleanup and add to attachments list
|
|
1629
|
+
this.clipboardImageFiles.push(filePath);
|
|
1630
|
+
this.attachments.push({ path: filePath, mimeType: image.mimeType });
|
|
1631
|
+
this.updateAttachmentsBar();
|
|
1622
1632
|
this.ui.requestRender();
|
|
1623
1633
|
}
|
|
1624
1634
|
catch {
|
|
1625
1635
|
// Silently ignore clipboard errors (may not have permission, etc.)
|
|
1626
1636
|
}
|
|
1627
1637
|
}
|
|
1638
|
+
updateAttachmentsBar() {
|
|
1639
|
+
if (!this.attachmentsContainer)
|
|
1640
|
+
return;
|
|
1641
|
+
this.attachmentsContainer.clear();
|
|
1642
|
+
if (this.attachments.length === 0) {
|
|
1643
|
+
this.attachmentsBar = undefined;
|
|
1644
|
+
this.editorContainer.removeChild(this.attachmentsContainer);
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
// Ensure attachmentsContainer is placed before the editor in the layout
|
|
1648
|
+
if (!this.editorContainer.children.includes(this.attachmentsContainer)) {
|
|
1649
|
+
const editorIdx = this.editorContainer.children.indexOf(this.editor);
|
|
1650
|
+
if (editorIdx >= 0) {
|
|
1651
|
+
this.editorContainer.children.splice(editorIdx, 0, this.attachmentsContainer);
|
|
1652
|
+
}
|
|
1653
|
+
else {
|
|
1654
|
+
this.editorContainer.addChild(this.attachmentsContainer);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
const themeName = this.settingsManager.getTheme();
|
|
1658
|
+
const theme = getThemeByName(themeName || "dark") ?? getThemeByName("dark");
|
|
1659
|
+
this.attachmentsBar = new AttachmentsBarComponent(this.attachments, this.selectedAttachmentIndex, theme);
|
|
1660
|
+
this.attachmentsContainer.addChild(this.attachmentsBar);
|
|
1661
|
+
}
|
|
1662
|
+
deleteAttachment(index) {
|
|
1663
|
+
if (index < 0 || index >= this.attachments.length)
|
|
1664
|
+
return;
|
|
1665
|
+
// Remove the attachment file
|
|
1666
|
+
const attachment = this.attachments[index];
|
|
1667
|
+
try {
|
|
1668
|
+
fs.unlinkSync(attachment.path);
|
|
1669
|
+
}
|
|
1670
|
+
catch {
|
|
1671
|
+
// Ignore file deletion errors
|
|
1672
|
+
}
|
|
1673
|
+
this.attachments.splice(index, 1);
|
|
1674
|
+
if (this.selectedAttachmentIndex >= this.attachments.length) {
|
|
1675
|
+
this.selectedAttachmentIndex = this.attachments.length - 1;
|
|
1676
|
+
}
|
|
1677
|
+
this.updateAttachmentsBar();
|
|
1678
|
+
this.ui.requestRender();
|
|
1679
|
+
}
|
|
1680
|
+
handleAttachmentKeyNavigation(data) {
|
|
1681
|
+
if (this.attachments.length === 0)
|
|
1682
|
+
return false;
|
|
1683
|
+
// Only intercept up/down arrows when multiple attachments need navigation.
|
|
1684
|
+
// With a single attachment, let the editor handle arrows for history browsing.
|
|
1685
|
+
if (this.attachments.length > 1) {
|
|
1686
|
+
if (matchesKey(data, "up")) {
|
|
1687
|
+
if (this.selectedAttachmentIndex < 0) {
|
|
1688
|
+
this.selectedAttachmentIndex = 0;
|
|
1689
|
+
}
|
|
1690
|
+
else if (this.selectedAttachmentIndex > 0) {
|
|
1691
|
+
this.selectedAttachmentIndex--;
|
|
1692
|
+
}
|
|
1693
|
+
this.updateAttachmentsBar();
|
|
1694
|
+
this.ui.requestRender();
|
|
1695
|
+
return true;
|
|
1696
|
+
}
|
|
1697
|
+
if (matchesKey(data, "down")) {
|
|
1698
|
+
if (this.selectedAttachmentIndex < 0) {
|
|
1699
|
+
this.selectedAttachmentIndex = 0;
|
|
1700
|
+
}
|
|
1701
|
+
else if (this.selectedAttachmentIndex < this.attachments.length - 1) {
|
|
1702
|
+
this.selectedAttachmentIndex++;
|
|
1703
|
+
}
|
|
1704
|
+
this.updateAttachmentsBar();
|
|
1705
|
+
this.ui.requestRender();
|
|
1706
|
+
return true;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
// Delete/backspace only removes attachment when one is explicitly selected
|
|
1710
|
+
if (this.selectedAttachmentIndex >= 0 &&
|
|
1711
|
+
(matchesKey(data, "delete") || matchesKey(data, "backspace"))) {
|
|
1712
|
+
this.deleteAttachment(this.selectedAttachmentIndex);
|
|
1713
|
+
return true;
|
|
1714
|
+
}
|
|
1715
|
+
return false;
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Convert attachment files to ImageContent array for sending to the model.
|
|
1719
|
+
* Reads each file, base64-encodes, and resizes.
|
|
1720
|
+
*/
|
|
1721
|
+
async processAttachmentFiles(attachments) {
|
|
1722
|
+
const result = [];
|
|
1723
|
+
for (const attachment of attachments) {
|
|
1724
|
+
try {
|
|
1725
|
+
if (!fs.existsSync(attachment.path))
|
|
1726
|
+
continue;
|
|
1727
|
+
const mimeType = attachment.mimeType ??
|
|
1728
|
+
(await detectSupportedImageMimeTypeFromFile(attachment.path));
|
|
1729
|
+
if (!mimeType)
|
|
1730
|
+
continue;
|
|
1731
|
+
const content = fs.readFileSync(attachment.path);
|
|
1732
|
+
const base64Content = content.toString("base64");
|
|
1733
|
+
const resized = await resizeImage({
|
|
1734
|
+
type: "image",
|
|
1735
|
+
data: base64Content,
|
|
1736
|
+
mimeType,
|
|
1737
|
+
});
|
|
1738
|
+
result.push({
|
|
1739
|
+
type: "image",
|
|
1740
|
+
mimeType: resized.mimeType,
|
|
1741
|
+
data: resized.data,
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
catch {
|
|
1745
|
+
// Skip unreadable attachment files
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
return result;
|
|
1749
|
+
}
|
|
1628
1750
|
/**
|
|
1629
1751
|
* Extract image file paths from text, read them as base64 ImageContent,
|
|
1630
1752
|
* and return the cleaned text with image references plus the image array.
|
|
@@ -1886,11 +2008,34 @@ export class InteractiveMode {
|
|
|
1886
2008
|
this.editor.addToHistory?.(text);
|
|
1887
2009
|
this.editor.setText("");
|
|
1888
2010
|
const steerResult = await this.extractImagesFromText(text);
|
|
1889
|
-
|
|
2011
|
+
const steerImages = steerResult.images;
|
|
2012
|
+
let steerAttachmentPaths = [];
|
|
2013
|
+
if (this.attachments.length > 0) {
|
|
2014
|
+
const pendingAttachments = this.attachments.splice(0);
|
|
2015
|
+
this.selectedAttachmentIndex = -1;
|
|
2016
|
+
this.updateAttachmentsBar();
|
|
2017
|
+
this.ui.requestRender();
|
|
2018
|
+
steerAttachmentPaths = pendingAttachments.map((a) => a.path);
|
|
2019
|
+
}
|
|
2020
|
+
// Drop images if model doesn't support them
|
|
2021
|
+
const steerModel = this.session.model;
|
|
2022
|
+
if ((steerImages.length > 0 || steerAttachmentPaths.length > 0) &&
|
|
2023
|
+
steerModel &&
|
|
2024
|
+
!steerModel.input.includes("image")) {
|
|
2025
|
+
steerImages.length = 0;
|
|
2026
|
+
steerAttachmentPaths = [];
|
|
2027
|
+
}
|
|
2028
|
+
let steerPromptText = steerResult.text;
|
|
2029
|
+
if (steerAttachmentPaths.length > 0) {
|
|
2030
|
+
const cwd = this.session.cwd;
|
|
2031
|
+
const refs = steerAttachmentPaths
|
|
2032
|
+
.map((p) => `@${path.relative(cwd, p).replace(/\\/g, "/")}`)
|
|
2033
|
+
.join(" ");
|
|
2034
|
+
steerPromptText = refs + " " + steerPromptText;
|
|
2035
|
+
}
|
|
2036
|
+
await this.session.prompt(steerPromptText, {
|
|
1890
2037
|
streamingBehavior: "steer",
|
|
1891
|
-
images:
|
|
1892
|
-
? steerResult.images
|
|
1893
|
-
: undefined,
|
|
2038
|
+
images: steerImages.length > 0 ? steerImages : undefined,
|
|
1894
2039
|
});
|
|
1895
2040
|
this.updatePendingMessagesDisplay();
|
|
1896
2041
|
this.ui.requestRender();
|
|
@@ -1908,6 +2053,37 @@ export class InteractiveMode {
|
|
|
1908
2053
|
this.editor.setText("");
|
|
1909
2054
|
// Extract images from clipboard-pasted file paths in the text
|
|
1910
2055
|
const { text: processedText, images } = await this.extractImagesFromText(text);
|
|
2056
|
+
// Collect and clear pending attachments upfront (ensures cleanup even on error).
|
|
2057
|
+
// Clipboard images are saved under .pi/images/ so models can read them via
|
|
2058
|
+
// file tools — this is more reliable than inline base64 (some models like
|
|
2059
|
+
// Kimi K2.5 re-map inline images to internal server paths that break MCP).
|
|
2060
|
+
let attachmentPaths = [];
|
|
2061
|
+
if (this.attachments.length > 0) {
|
|
2062
|
+
const pendingAttachments = this.attachments.splice(0);
|
|
2063
|
+
this.selectedAttachmentIndex = -1;
|
|
2064
|
+
this.updateAttachmentsBar();
|
|
2065
|
+
this.ui.requestRender();
|
|
2066
|
+
attachmentPaths = pendingAttachments.map((a) => a.path);
|
|
2067
|
+
}
|
|
2068
|
+
// Check model image support; warn and drop images if not supported
|
|
2069
|
+
if (images.length > 0 || attachmentPaths.length > 0) {
|
|
2070
|
+
const currentModel = this.session.model;
|
|
2071
|
+
if (currentModel && !currentModel.input.includes("image")) {
|
|
2072
|
+
this.showWarning(`Model "${currentModel.name}" does not support image input. Images have been removed from this message.`);
|
|
2073
|
+
images.length = 0;
|
|
2074
|
+
attachmentPaths = [];
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
// Build image references using the relative path (_paste/image_N.ext).
|
|
2078
|
+
// Prepended before user text so the model processes it first.
|
|
2079
|
+
let imageRefPrefix = "";
|
|
2080
|
+
if (attachmentPaths.length > 0) {
|
|
2081
|
+
const cwd = this.session.cwd;
|
|
2082
|
+
const refs = attachmentPaths
|
|
2083
|
+
.map((p) => `@${path.relative(cwd, p).replace(/\\/g, "/")}`)
|
|
2084
|
+
.join(" ");
|
|
2085
|
+
imageRefPrefix = refs + " ";
|
|
2086
|
+
}
|
|
1911
2087
|
if (!processedText.startsWith("/")) {
|
|
1912
2088
|
const displayContent = [
|
|
1913
2089
|
{ type: "text", text: processedText },
|
|
@@ -1926,7 +2102,10 @@ export class InteractiveMode {
|
|
|
1926
2102
|
try {
|
|
1927
2103
|
// Clear persona switch flag - interview should now run normally for subsequent messages
|
|
1928
2104
|
delete process.env.PI_JUST_SWITCHED_PERSONA;
|
|
1929
|
-
|
|
2105
|
+
const promptText = imageRefPrefix
|
|
2106
|
+
? imageRefPrefix + processedText
|
|
2107
|
+
: processedText;
|
|
2108
|
+
await this.session.prompt(promptText, {
|
|
1930
2109
|
images: images.length > 0 ? images : undefined,
|
|
1931
2110
|
});
|
|
1932
2111
|
}
|
|
@@ -1941,8 +2120,67 @@ export class InteractiveMode {
|
|
|
1941
2120
|
}
|
|
1942
2121
|
this.updatePendingMessagesDisplay();
|
|
1943
2122
|
this.ui.requestRender();
|
|
2123
|
+
// Clean up temporary clipboard image files from project root
|
|
2124
|
+
this.cleanupClipboardImages();
|
|
1944
2125
|
};
|
|
1945
2126
|
}
|
|
2127
|
+
cleanupStaleClipboardFiles() {
|
|
2128
|
+
try {
|
|
2129
|
+
const cwd = this.session.cwd;
|
|
2130
|
+
// Clean old-format root files (_clipboard_*.png)
|
|
2131
|
+
for (const entry of fs.readdirSync(cwd)) {
|
|
2132
|
+
if (/^_clipboard_\d+\.\w+$/.test(entry)) {
|
|
2133
|
+
try {
|
|
2134
|
+
fs.unlinkSync(path.join(cwd, entry));
|
|
2135
|
+
}
|
|
2136
|
+
catch { /* best-effort */ }
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
// Clean _paste/ directory from previous sessions
|
|
2140
|
+
const pasteDir = path.join(cwd, "_paste");
|
|
2141
|
+
if (fs.existsSync(pasteDir)) {
|
|
2142
|
+
for (const entry of fs.readdirSync(pasteDir)) {
|
|
2143
|
+
try {
|
|
2144
|
+
fs.unlinkSync(path.join(pasteDir, entry));
|
|
2145
|
+
}
|
|
2146
|
+
catch { /* best-effort */ }
|
|
2147
|
+
}
|
|
2148
|
+
try {
|
|
2149
|
+
fs.rmdirSync(pasteDir);
|
|
2150
|
+
}
|
|
2151
|
+
catch { /* best-effort */ }
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
catch {
|
|
2155
|
+
// Ignore errors during cleanup
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
cleanupClipboardImages() {
|
|
2159
|
+
for (const filePath of this.clipboardImageFiles) {
|
|
2160
|
+
try {
|
|
2161
|
+
if (fs.existsSync(filePath)) {
|
|
2162
|
+
fs.unlinkSync(filePath);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
catch {
|
|
2166
|
+
// Best-effort cleanup
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
this.clipboardImageFiles = [];
|
|
2170
|
+
// Remove _paste/ directory if empty
|
|
2171
|
+
try {
|
|
2172
|
+
const pasteDir = path.join(this.session.cwd, "_paste");
|
|
2173
|
+
if (fs.existsSync(pasteDir)) {
|
|
2174
|
+
const remaining = fs.readdirSync(pasteDir);
|
|
2175
|
+
if (remaining.length === 0) {
|
|
2176
|
+
fs.rmdirSync(pasteDir);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
catch {
|
|
2181
|
+
// Best-effort
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
1946
2184
|
subscribeToAgent() {
|
|
1947
2185
|
this.unsubscribe = this.session.subscribe(async (event) => {
|
|
1948
2186
|
await this.handleEvent(event);
|
|
@@ -3342,7 +3580,31 @@ export class InteractiveMode {
|
|
|
3342
3580
|
}, () => {
|
|
3343
3581
|
done();
|
|
3344
3582
|
this.ui.requestRender();
|
|
3345
|
-
}, initialSearchInput, filterByProvider)
|
|
3583
|
+
}, initialSearchInput, filterByProvider, () => {
|
|
3584
|
+
void (async () => {
|
|
3585
|
+
done();
|
|
3586
|
+
const modelId = await this.showExtensionInput("Add OpenRouter model", "Model id (e.g. x-ai/grok-4.20)");
|
|
3587
|
+
if (!modelId?.trim()) {
|
|
3588
|
+
this.showModelSelector(initialSearchInput, filterByProvider);
|
|
3589
|
+
return;
|
|
3590
|
+
}
|
|
3591
|
+
const nameInput = await this.showExtensionInput("Display name (optional)", "Leave empty to use model id", { initialValue: modelId.trim() });
|
|
3592
|
+
if (nameInput === undefined) {
|
|
3593
|
+
this.showModelSelector(initialSearchInput, filterByProvider);
|
|
3594
|
+
return;
|
|
3595
|
+
}
|
|
3596
|
+
try {
|
|
3597
|
+
this.session.modelRegistry.appendOpenRouterModel(modelId.trim(), {
|
|
3598
|
+
name: nameInput.trim() || undefined,
|
|
3599
|
+
});
|
|
3600
|
+
this.showStatus(`Added OpenRouter model ${modelId.trim()}`);
|
|
3601
|
+
}
|
|
3602
|
+
catch (error) {
|
|
3603
|
+
this.showError(error instanceof Error ? error.message : String(error));
|
|
3604
|
+
}
|
|
3605
|
+
this.showModelSelector(initialSearchInput, filterByProvider);
|
|
3606
|
+
})();
|
|
3607
|
+
});
|
|
3346
3608
|
return { component: selector, focus: selector };
|
|
3347
3609
|
});
|
|
3348
3610
|
}
|
|
@@ -75,6 +75,12 @@ export declare const NANOPENCIL_DEFAULT_MODELS_JSON: {
|
|
|
75
75
|
readonly baseUrl: "https://api.minimaxi.com/anthropic";
|
|
76
76
|
readonly api: "openai-completions";
|
|
77
77
|
readonly models: readonly [{
|
|
78
|
+
readonly id: "MiniMax-M2.7";
|
|
79
|
+
readonly name: "MiniMax M2.7";
|
|
80
|
+
readonly input: readonly ["text"];
|
|
81
|
+
readonly contextWindow: 204800;
|
|
82
|
+
readonly maxTokens: 65536;
|
|
83
|
+
}, {
|
|
78
84
|
readonly id: "MiniMax-M2.5";
|
|
79
85
|
readonly name: "MiniMax M2.5";
|
|
80
86
|
readonly input: readonly ["text"];
|
|
@@ -99,6 +99,13 @@ export const NANOPENCIL_DEFAULT_MODELS_JSON = {
|
|
|
99
99
|
baseUrl: MINIMAX_CODING_BASE_URL,
|
|
100
100
|
api: "openai-completions",
|
|
101
101
|
models: [
|
|
102
|
+
{
|
|
103
|
+
id: "MiniMax-M2.7",
|
|
104
|
+
name: "MiniMax M2.7",
|
|
105
|
+
input: ["text"],
|
|
106
|
+
contextWindow: 204800,
|
|
107
|
+
maxTokens: 65536,
|
|
108
|
+
},
|
|
102
109
|
{
|
|
103
110
|
id: "MiniMax-M2.5",
|
|
104
111
|
name: "MiniMax M2.5",
|
|
@@ -461,6 +468,58 @@ function mergeNanopencilModelsIfNeeded(modelsPath) {
|
|
|
461
468
|
writeFileSync(modelsPath, JSON.stringify(data, null, 2), "utf-8");
|
|
462
469
|
}
|
|
463
470
|
}
|
|
471
|
+
// 合并 minimax-coding:不存在则添加默认配置,存在则补充默认模型
|
|
472
|
+
const minimaxProvider = data.providers[NANOPENCIL_MINIMAX_CODING_PROVIDER];
|
|
473
|
+
const minimaxConfig = NANOPENCIL_DEFAULT_MODELS_JSON.providers[NANOPENCIL_MINIMAX_CODING_PROVIDER];
|
|
474
|
+
if (!minimaxProvider) {
|
|
475
|
+
data.providers[NANOPENCIL_MINIMAX_CODING_PROVIDER] = {
|
|
476
|
+
baseUrl: minimaxConfig.baseUrl,
|
|
477
|
+
api: minimaxConfig.api,
|
|
478
|
+
models: DEFAULT_MINIMAX_MODELS.map((m) => ({ ...m })),
|
|
479
|
+
};
|
|
480
|
+
writeFileSync(modelsPath, JSON.stringify(data, null, 2), "utf-8");
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
const minimaxModels = (Array.isArray(minimaxProvider.models) ? [...minimaxProvider.models] : []);
|
|
484
|
+
const minimaxById = new Map();
|
|
485
|
+
for (const m of minimaxModels) {
|
|
486
|
+
const id = m?.id;
|
|
487
|
+
if (typeof id === "string")
|
|
488
|
+
minimaxById.set(id, m);
|
|
489
|
+
}
|
|
490
|
+
let minimaxChanged = false;
|
|
491
|
+
for (const def of DEFAULT_MINIMAX_MODELS) {
|
|
492
|
+
const id = def.id;
|
|
493
|
+
const existing = minimaxById.get(id);
|
|
494
|
+
if (!existing) {
|
|
495
|
+
minimaxModels.push({ ...def });
|
|
496
|
+
minimaxById.set(id, minimaxModels[minimaxModels.length - 1]);
|
|
497
|
+
minimaxChanged = true;
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
if (existing.contextWindow !== def.contextWindow) {
|
|
501
|
+
existing.contextWindow = def.contextWindow;
|
|
502
|
+
minimaxChanged = true;
|
|
503
|
+
}
|
|
504
|
+
if (existing.maxTokens !== def.maxTokens) {
|
|
505
|
+
existing.maxTokens = def.maxTokens;
|
|
506
|
+
minimaxChanged = true;
|
|
507
|
+
}
|
|
508
|
+
if (JSON.stringify(existing.input) !== JSON.stringify(def.input)) {
|
|
509
|
+
existing.input = def.input;
|
|
510
|
+
minimaxChanged = true;
|
|
511
|
+
}
|
|
512
|
+
if (existing.name !== def.name) {
|
|
513
|
+
existing.name = def.name;
|
|
514
|
+
minimaxChanged = true;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (minimaxChanged) {
|
|
519
|
+
data.providers[NANOPENCIL_MINIMAX_CODING_PROVIDER].models = minimaxModels;
|
|
520
|
+
writeFileSync(modelsPath, JSON.stringify(data, null, 2), "utf-8");
|
|
521
|
+
}
|
|
522
|
+
}
|
|
464
523
|
// 合并 ollama:不存在则添加默认配置,存在则补充默认模型
|
|
465
524
|
const ollamaProvider = data.providers[NANOPENCIL_OLLAMA_PROVIDER];
|
|
466
525
|
const ollamaConfig = NANOPENCIL_DEFAULT_MODELS_JSON.providers[NANOPENCIL_OLLAMA_PROVIDER];
|
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.33",
|
|
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": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"bundle:packages": "node scripts/bundle-deps.js",
|
|
30
30
|
"watch": "tsc --watch",
|
|
31
31
|
"test:presence": "node --test --import tsx test/presence-opening.test.ts",
|
|
32
|
+
"test:interactive-memory-notify": "node --test --import tsx test/interactive-memory-notify.test.ts",
|
|
32
33
|
"start": "npx cross-env NODE_ENV=production node --no-deprecation dist/cli.js",
|
|
33
34
|
"prepublishOnly": "npm run build",
|
|
34
35
|
"changelog": "node scripts/generate-changelog.js",
|