@pencil-agent/nano-pencil 1.13.6 → 1.13.7
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/build-meta.json +3 -3
- package/dist/core/model-registry.js +2 -1
- package/dist/core/runtime/agent-session.js +29 -4
- package/dist/extensions/defaults/AGENT.md +1 -1
- package/dist/extensions/defaults/CLAUDE.md +1 -1
- package/dist/extensions/defaults/sal/eval/insforge-sink.d.ts +1 -1
- package/dist/extensions/defaults/sal/eval/insforge-sink.js +49 -27
- package/dist/modes/interactive/components/footer.d.ts +4 -3
- package/dist/modes/interactive/components/footer.js +16 -8
- package/dist/modes/interactive/components/provider-selector.d.ts +18 -5
- package/dist/modes/interactive/components/provider-selector.js +128 -21
- package/dist/modes/interactive/interactive-mode.js +4 -9
- package/dist/node_modules/@pencil-agent/ai/cli.js +0 -0
- package/dist/node_modules/@pencil-agent/ai/models.generated.d.ts +358 -1
- package/dist/node_modules/@pencil-agent/ai/models.generated.js +401 -44
- package/package.json +1 -1
package/dist/build-meta.json
CHANGED
|
@@ -207,8 +207,9 @@ export class ModelRegistry {
|
|
|
207
207
|
// Keep built-in models even if custom models failed to load
|
|
208
208
|
}
|
|
209
209
|
const builtInModels = this.useOnlyCustomModels
|
|
210
|
-
? this.loadBuiltInModels(overrides, modelOverrides, new Set(["openrouter"]), {
|
|
210
|
+
? this.loadBuiltInModels(overrides, modelOverrides, new Set(["openrouter", "zai"]), {
|
|
211
211
|
openrouter: new Set(NANOPENCIL_OPENROUTER_BUILTIN_MODEL_IDS),
|
|
212
|
+
// zai not specified = load all zai models
|
|
212
213
|
})
|
|
213
214
|
: this.loadBuiltInModels(overrides, modelOverrides);
|
|
214
215
|
let combined = this.mergeCustomModels(builtInModels, customModels);
|
|
@@ -1183,8 +1183,22 @@ export class AgentSession {
|
|
|
1183
1183
|
this.agent.setModel(model);
|
|
1184
1184
|
this.sessionManager.appendModelChange(model.provider, model.id);
|
|
1185
1185
|
this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
|
|
1186
|
-
//
|
|
1187
|
-
this.
|
|
1186
|
+
// Auto-select thinking level based on model capabilities
|
|
1187
|
+
const currentLevel = this.thinkingLevel;
|
|
1188
|
+
let newLevel;
|
|
1189
|
+
if (!model.reasoning) {
|
|
1190
|
+
// Model doesn't support thinking, force off
|
|
1191
|
+
newLevel = "off";
|
|
1192
|
+
}
|
|
1193
|
+
else if (currentLevel === "off") {
|
|
1194
|
+
// Model supports thinking but current level is off, default to medium
|
|
1195
|
+
newLevel = "medium";
|
|
1196
|
+
}
|
|
1197
|
+
else {
|
|
1198
|
+
// Keep current level but clamp to new model's capabilities
|
|
1199
|
+
newLevel = currentLevel;
|
|
1200
|
+
}
|
|
1201
|
+
this.setThinkingLevel(newLevel);
|
|
1188
1202
|
await this._emitModelSelect(model, previousModel, "set");
|
|
1189
1203
|
}
|
|
1190
1204
|
/**
|
|
@@ -1290,8 +1304,19 @@ export class AgentSession {
|
|
|
1290
1304
|
this.agent.setModel(nextModel);
|
|
1291
1305
|
this.sessionManager.appendModelChange(nextModel.provider, nextModel.id);
|
|
1292
1306
|
this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
|
|
1293
|
-
//
|
|
1294
|
-
this.
|
|
1307
|
+
// Auto-select thinking level based on model capabilities
|
|
1308
|
+
const currentLevel = this.thinkingLevel;
|
|
1309
|
+
let newLevel;
|
|
1310
|
+
if (!nextModel.reasoning) {
|
|
1311
|
+
newLevel = "off";
|
|
1312
|
+
}
|
|
1313
|
+
else if (currentLevel === "off") {
|
|
1314
|
+
newLevel = "medium";
|
|
1315
|
+
}
|
|
1316
|
+
else {
|
|
1317
|
+
newLevel = currentLevel;
|
|
1318
|
+
}
|
|
1319
|
+
this.setThinkingLevel(newLevel);
|
|
1295
1320
|
await this._emitModelSelect(nextModel, currentModel, "cycle");
|
|
1296
1321
|
return {
|
|
1297
1322
|
model: nextModel,
|
|
@@ -44,7 +44,7 @@ sal/weights.ts: SalWeights interface, SAL_DEFAULT_WEIGHTS, loadSalWeights() read
|
|
|
44
44
|
sal/eval/index.ts: createEvalSink() factory + barrel re-exports; adapter selection via options.adapter or endpoint scheme inference (http(s)→insforge, file://|/|./|../→jsonl, missing→noop); ONLY entry point SAL imports from
|
|
45
45
|
sal/eval/types.ts: EvalSink interface, EvalEventEnvelope/EvalEventType (run_start/run_end/turn_anchor), EvalAdapterId ("insforge"|"jsonl"|"noop"), CreateEvalSinkOptions, createEvalEvent factory; zero-dependency type surface
|
|
46
46
|
sal/eval/noop-sink.ts: noopSink — silent EvalSink used when eval disabled or no adapter configured
|
|
47
|
-
sal/eval/insforge-sink.ts: InsForgeEvalSink — PostgREST adapter, routes run_start→eval_runs INSERT (merge-duplicates), turn_anchor→eval_turns + eval_sal_anchors×2, run_end→eval_runs PATCH; allowSelfSigned TLS option, batching with default 2000ms interval
|
|
47
|
+
sal/eval/insforge-sink.ts: InsForgeEvalSink — PostgREST adapter, routes run_start→eval_runs INSERT (merge-duplicates), turn_anchor→eval_turns + eval_sal_anchors×2, tool_trace→eval_tool_traces with PGRST204 legacy-schema fallback, memory_recalls→eval_memory_recalls batch INSERT, run_end→eval_runs PATCH; allowSelfSigned TLS option, batching with default 2000ms interval
|
|
48
48
|
sal/eval/jsonl-sink.ts: JsonlEvalSink — append-only filesystem adapter, one JSON object per line, accepts file:// URLs or plain paths, auto-creates parent dir, batched writes
|
|
49
49
|
sal/README.md: SAL extension usage, sidecar output layout, weights override, pluggability contract
|
|
50
50
|
team/index.ts: AgentTeam extension entry, /team:/team:spawn/:send/:status/:stop/:terminate/:approve/:mode commands, TEAM_MESSAGE_TYPE renderer
|
|
@@ -47,7 +47,7 @@ sal/terrain.ts: TerrainSnapshot/TerrainNode/TerrainEdge model, async buildTerrai
|
|
|
47
47
|
sal/anchors.ts: StructuralAnchor/AnchorResolution model, locateTask(), locateAction(), evidence-driven scoring with tunable SalWeights, CJK bigram tokenization
|
|
48
48
|
sal/weights.ts: SalWeights interface, SAL_DEFAULT_WEIGHTS, loadSalWeights() reads sal-config.json from workspace or .memory-experiments/sal/
|
|
49
49
|
sal/eval/index.ts: createEvalSink() factory + barrel re-exports; adapter selection via options.adapter or endpoint scheme inference (http(s)→insforge, file://|/|./|../→jsonl, missing→noop); ONLY entry point SAL imports from
|
|
50
|
-
sal/eval/types.ts: EvalSink interface, EvalEventEnvelope/EvalEventType (run_start/run_end/turn_anchor/memory_recalls), EvalAdapterId ("insforge"|"jsonl"|"noop"), CreateEvalSinkOptions, createEvalEvent factory; zero-dependency type surface
|
|
50
|
+
sal/eval/types.ts: EvalSink interface, EvalEventEnvelope/EvalEventType (run_start/run_end/turn_anchor/memory_recalls/tool_trace), EvalAdapterId ("insforge"|"jsonl"|"noop"), CreateEvalSinkOptions, createEvalEvent factory; zero-dependency type surface
|
|
51
51
|
sal/eval/noop-sink.ts: noopSink — silent EvalSink used when eval disabled or no adapter configured
|
|
52
52
|
sal/eval/insforge-sink.ts: InsForgeEvalSink — PostgREST adapter, routes run_start→eval_runs INSERT (merge-duplicates), turn_anchor→eval_turns + eval_sal_anchors×2, tool_trace→eval_tool_traces bounded per-turn summaries (including no-tool turns and truncation counters), memory_recalls→eval_memory_recalls batch INSERT, run_end→eval_runs PATCH; allowSelfSigned TLS option, batching with default 2000ms interval
|
|
53
53
|
sal/eval/jsonl-sink.ts: JsonlEvalSink — append-only filesystem adapter, one JSON object per line, accepts file:// URLs or plain paths, auto-creates parent dir, batched writes
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* [WHO]: Provides InsForgeEvalSink (PostgREST-backed adapter)
|
|
3
3
|
* [FROM]: Depends on node:https, node:http, node:url; ./types.js for EvalSink/EvalEventEnvelope/CreateEvalSinkOptions
|
|
4
4
|
* [TO]: Constructed by eval/index.ts factory when adapter resolves to "insforge"
|
|
5
|
-
* [HERE]: extensions/defaults/sal/eval/insforge-sink.ts - InsForge-specific routing: run_start→eval_runs INSERT (merge-duplicates, includes pencil_version), turn_anchor→eval_turns + eval_sal_anchors×2, tool_trace→eval_tool_traces, memory_recalls→eval_memory_recalls, run_end→eval_runs PATCH
|
|
5
|
+
* [HERE]: extensions/defaults/sal/eval/insforge-sink.ts - InsForge-specific routing: run_start→eval_runs INSERT (merge-duplicates, includes pencil_version), turn_anchor→eval_turns + eval_sal_anchors×2, tool_trace→eval_tool_traces with legacy-schema fallback, memory_recalls→eval_memory_recalls, run_end→eval_runs PATCH
|
|
6
6
|
*
|
|
7
7
|
* Pluggable: nothing in this file may be imported from outside the eval/ directory.
|
|
8
8
|
* To add a new backend, write a sibling file with the same EvalSink interface.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* [WHO]: Provides InsForgeEvalSink (PostgREST-backed adapter)
|
|
3
3
|
* [FROM]: Depends on node:https, node:http, node:url; ./types.js for EvalSink/EvalEventEnvelope/CreateEvalSinkOptions
|
|
4
4
|
* [TO]: Constructed by eval/index.ts factory when adapter resolves to "insforge"
|
|
5
|
-
* [HERE]: extensions/defaults/sal/eval/insforge-sink.ts - InsForge-specific routing: run_start→eval_runs INSERT (merge-duplicates, includes pencil_version), turn_anchor→eval_turns + eval_sal_anchors×2, tool_trace→eval_tool_traces, memory_recalls→eval_memory_recalls, run_end→eval_runs PATCH
|
|
5
|
+
* [HERE]: extensions/defaults/sal/eval/insforge-sink.ts - InsForge-specific routing: run_start→eval_runs INSERT (merge-duplicates, includes pencil_version), turn_anchor→eval_turns + eval_sal_anchors×2, tool_trace→eval_tool_traces with legacy-schema fallback, memory_recalls→eval_memory_recalls, run_end→eval_runs PATCH
|
|
6
6
|
*
|
|
7
7
|
* Pluggable: nothing in this file may be imported from outside the eval/ directory.
|
|
8
8
|
* To add a new backend, write a sibling file with the same EvalSink interface.
|
|
@@ -222,25 +222,33 @@ export class InsForgeEvalSink {
|
|
|
222
222
|
async handleToolTrace(ev) {
|
|
223
223
|
const p = ev.payload;
|
|
224
224
|
const taskSignals = p.task_signals;
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
225
|
+
const row = {
|
|
226
|
+
run_id: ev.run_id,
|
|
227
|
+
turn_id: String(p.turn_id ?? 0),
|
|
228
|
+
event_id: ev.event_id,
|
|
229
|
+
tool_calls: p.tool_calls ? JSON.stringify(p.tool_calls) : null,
|
|
230
|
+
tool_sequence: p.tool_sequence ? JSON.stringify(p.tool_sequence) : null,
|
|
231
|
+
intent: strOrNull(taskSignals?.intent),
|
|
232
|
+
prompt_length: String(taskSignals?.prompt_length ?? 0),
|
|
233
|
+
has_error_trace: String(taskSignals?.has_error_trace === true),
|
|
234
|
+
has_file_reference: String(taskSignals?.has_file_reference === true),
|
|
235
|
+
has_tool_usage: String(p.has_tool_usage === true),
|
|
236
|
+
total_tool_calls: String(p.total_tool_calls ?? 0),
|
|
237
|
+
total_errors: String(p.total_errors ?? 0),
|
|
238
|
+
completed_tool_calls: String(p.completed_tool_calls ?? 0),
|
|
239
|
+
truncated_tool_calls: String(p.truncated_tool_calls ?? 0),
|
|
240
|
+
truncated_tool_summary: String(p.truncated_tool_summary ?? 0),
|
|
241
|
+
duration_ms: String(p.duration_ms ?? 0),
|
|
242
|
+
recorded_at: ev.ts,
|
|
243
|
+
};
|
|
244
|
+
const url = `${this.base}/api/database/records/eval_tool_traces`;
|
|
245
|
+
const result = await this.postJson(url, [row], {
|
|
246
|
+
prefer: "resolution=ignore-duplicates",
|
|
247
|
+
quietErrorCodes: ["PGRST204"],
|
|
248
|
+
});
|
|
249
|
+
if (!result.ok && result.errorCode === "PGRST204") {
|
|
250
|
+
await this.postJson(url, [toLegacyToolTraceRow(row)], { prefer: "resolution=ignore-duplicates" });
|
|
251
|
+
}
|
|
244
252
|
}
|
|
245
253
|
// ------------------------------------------------------------------
|
|
246
254
|
// HTTP helpers
|
|
@@ -249,12 +257,12 @@ export class InsForgeEvalSink {
|
|
|
249
257
|
const extraHeaders = {};
|
|
250
258
|
if (extra?.prefer)
|
|
251
259
|
extraHeaders["Prefer"] = extra.prefer;
|
|
252
|
-
return this.httpJson("POST", url, body, extraHeaders);
|
|
260
|
+
return this.httpJson("POST", url, body, extraHeaders, extra?.quietErrorCodes);
|
|
253
261
|
}
|
|
254
262
|
patchJson(url, body) {
|
|
255
263
|
return this.httpJson("PATCH", url, body, {});
|
|
256
264
|
}
|
|
257
|
-
httpJson(method, url, body, extraHeaders) {
|
|
265
|
+
httpJson(method, url, body, extraHeaders, quietErrorCodes = []) {
|
|
258
266
|
return new Promise((resolve) => {
|
|
259
267
|
const payload = JSON.stringify(body);
|
|
260
268
|
let parsed;
|
|
@@ -263,7 +271,7 @@ export class InsForgeEvalSink {
|
|
|
263
271
|
}
|
|
264
272
|
catch {
|
|
265
273
|
console.error(`[sal][eval] invalid URL: ${url}`);
|
|
266
|
-
resolve(false);
|
|
274
|
+
resolve({ ok: false });
|
|
267
275
|
return;
|
|
268
276
|
}
|
|
269
277
|
const isHttps = parsed.protocol === "https:";
|
|
@@ -287,20 +295,21 @@ export class InsForgeEvalSink {
|
|
|
287
295
|
res.on("data", (chunk) => { rawBody += chunk; });
|
|
288
296
|
res.on("end", () => {
|
|
289
297
|
const ok = res.statusCode !== undefined && res.statusCode < 300;
|
|
290
|
-
|
|
298
|
+
const errorCode = parsePostgrestErrorCode(rawBody);
|
|
299
|
+
if (!ok && !quietErrorCodes.includes(errorCode ?? "")) {
|
|
291
300
|
console.error(`[sal][eval] HTTP ${res.statusCode} ${method} ${parsed.pathname} — ${rawBody.slice(0, 300)}`);
|
|
292
301
|
}
|
|
293
|
-
resolve(ok);
|
|
302
|
+
resolve({ ok, statusCode: res.statusCode, body: rawBody, errorCode });
|
|
294
303
|
});
|
|
295
304
|
});
|
|
296
305
|
req.on("error", (err) => {
|
|
297
306
|
console.error(`[sal][eval] network error → ${parsed.hostname}: ${err.message}`);
|
|
298
|
-
resolve(false);
|
|
307
|
+
resolve({ ok: false });
|
|
299
308
|
});
|
|
300
309
|
req.on("timeout", () => {
|
|
301
310
|
console.error(`[sal][eval] timeout ${method} ${parsed.pathname}`);
|
|
302
311
|
req.destroy();
|
|
303
|
-
resolve(false);
|
|
312
|
+
resolve({ ok: false });
|
|
304
313
|
});
|
|
305
314
|
req.write(payload);
|
|
306
315
|
req.end();
|
|
@@ -321,3 +330,16 @@ function numOrNull(v) {
|
|
|
321
330
|
const n = Number(v);
|
|
322
331
|
return isNaN(n) ? null : n;
|
|
323
332
|
}
|
|
333
|
+
function parsePostgrestErrorCode(rawBody) {
|
|
334
|
+
try {
|
|
335
|
+
const parsed = JSON.parse(rawBody);
|
|
336
|
+
return typeof parsed?.code === "string" ? parsed.code : undefined;
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function toLegacyToolTraceRow(row) {
|
|
343
|
+
const { has_tool_usage: _hasToolUsage, completed_tool_calls: _completedToolCalls, truncated_tool_calls: _truncatedToolCalls, truncated_tool_summary: _truncatedToolSummary, ...legacyRow } = row;
|
|
344
|
+
return legacyRow;
|
|
345
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* [WHO]: FooterComponent
|
|
2
|
+
* [WHO]: FooterComponent, renderContextProgressBar()
|
|
3
3
|
* [FROM]: Depends on @pencil-agent/tui, ../theme/theme.js
|
|
4
|
-
* [TO]: Consumed by modes/interactive/components/index.ts
|
|
5
|
-
* [HERE]: modes/interactive/components/footer.ts - status bar footer
|
|
4
|
+
* [TO]: Consumed by modes/interactive/components/index.ts, modes/interactive/interactive-mode.ts
|
|
5
|
+
* [HERE]: modes/interactive/components/footer.ts - status bar footer and shared context progress rendering
|
|
6
6
|
*/
|
|
7
7
|
import { type Component } from "@pencil-agent/tui";
|
|
8
8
|
import type { AgentSession } from "../../../core/runtime/agent-session.js";
|
|
9
9
|
import type { ReadonlyFooterDataProvider } from "../../../core/footer-data-provider.js";
|
|
10
|
+
export declare function renderContextProgressBar(contextPercent: number, barWidth?: number): string;
|
|
10
11
|
/**
|
|
11
12
|
* Footer component that shows pwd, token stats, and context usage.
|
|
12
13
|
* Computes token/context stats from session, gets git branch and extension statuses from provider.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* [WHO]: FooterComponent
|
|
2
|
+
* [WHO]: FooterComponent, renderContextProgressBar()
|
|
3
3
|
* [FROM]: Depends on @pencil-agent/tui, ../theme/theme.js
|
|
4
|
-
* [TO]: Consumed by modes/interactive/components/index.ts
|
|
5
|
-
* [HERE]: modes/interactive/components/footer.ts - status bar footer
|
|
4
|
+
* [TO]: Consumed by modes/interactive/components/index.ts, modes/interactive/interactive-mode.ts
|
|
5
|
+
* [HERE]: modes/interactive/components/footer.ts - status bar footer and shared context progress rendering
|
|
6
6
|
*/
|
|
7
7
|
import { truncateToWidth, visibleWidth } from "@pencil-agent/tui";
|
|
8
8
|
import { theme } from "../theme/theme.js";
|
|
@@ -31,6 +31,18 @@ function formatTokens(count) {
|
|
|
31
31
|
return `${(count / 1000000).toFixed(1)}M`;
|
|
32
32
|
return `${Math.round(count / 1000000)}M`;
|
|
33
33
|
}
|
|
34
|
+
export function renderContextProgressBar(contextPercent, barWidth = 12) {
|
|
35
|
+
const safeBarWidth = Math.max(0, Math.floor(barWidth));
|
|
36
|
+
const finitePercent = Number.isFinite(contextPercent) ? contextPercent : 0;
|
|
37
|
+
const clampedPercent = Math.min(100, Math.max(0, finitePercent));
|
|
38
|
+
const filled = Math.min(safeBarWidth, Math.max(0, Math.round((clampedPercent / 100) * safeBarWidth)));
|
|
39
|
+
const empty = Math.max(0, safeBarWidth - filled);
|
|
40
|
+
const fillColor = finitePercent > 90 ? "error" : finitePercent > 70 ? "warning" : "success";
|
|
41
|
+
return theme.fg("dim", "[") +
|
|
42
|
+
theme.fg(fillColor, "█".repeat(filled)) +
|
|
43
|
+
theme.fg("dim", "░".repeat(empty)) +
|
|
44
|
+
theme.fg("dim", "]");
|
|
45
|
+
}
|
|
34
46
|
/**
|
|
35
47
|
* Footer component that shows pwd, token stats, and context usage.
|
|
36
48
|
* Computes token/context stats from session, gets git branch and extension statuses from provider.
|
|
@@ -140,11 +152,7 @@ export class FooterComponent {
|
|
|
140
152
|
// Build progress bar for context usage (only on wide terminals)
|
|
141
153
|
let contextBar = "";
|
|
142
154
|
if (width > 80 && contextPercentValue > 0 && contextPercent !== "?") {
|
|
143
|
-
|
|
144
|
-
const filled = Math.round((contextPercentValue / 100) * barWidth);
|
|
145
|
-
const empty = barWidth - filled;
|
|
146
|
-
const fillColor = contextPercentValue > 90 ? "error" : contextPercentValue > 70 ? "warning" : "success";
|
|
147
|
-
contextBar = theme.fg("dim", "[") + theme.fg(fillColor, "█".repeat(filled)) + theme.fg("dim", "░".repeat(empty)) + theme.fg("dim", "] ");
|
|
155
|
+
contextBar = `${renderContextProgressBar(contextPercentValue)} `;
|
|
148
156
|
}
|
|
149
157
|
const contextPercentDisplay = contextPercent === "?" || contextTokens === null
|
|
150
158
|
? `${contextBar}?/${formatTokens(contextWindow)}${autoIndicator}`
|
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* [WHO]: ProviderSelectorComponent
|
|
3
3
|
* [FROM]: Depends on @pencil-agent/tui, ../theme/theme.js, ./dynamic-border.js
|
|
4
|
-
* [TO]: Consumed by modes/interactive/
|
|
4
|
+
* [TO]: Consumed by modes/interactive/interactive-mode.ts
|
|
5
5
|
* [HERE]: modes/interactive/components/provider-selector.ts -
|
|
6
|
+
* Adds search to provider selection when /model has many providers
|
|
6
7
|
*/
|
|
7
|
-
import { Container,
|
|
8
|
-
export declare class ProviderSelectorComponent extends Container {
|
|
9
|
-
private
|
|
8
|
+
import { type Focusable, Container, Input } from "@pencil-agent/tui";
|
|
9
|
+
export declare class ProviderSelectorComponent extends Container implements Focusable {
|
|
10
|
+
private searchInput;
|
|
11
|
+
private listContainer;
|
|
12
|
+
private allProviders;
|
|
13
|
+
private filteredProviders;
|
|
14
|
+
private selectedIndex;
|
|
15
|
+
private onSelectCallback;
|
|
16
|
+
private onCancelCallback;
|
|
17
|
+
private _focused;
|
|
18
|
+
get focused(): boolean;
|
|
19
|
+
set focused(value: boolean);
|
|
10
20
|
constructor(providers: string[], currentProvider: string | undefined, onSelect: (provider: string) => void, onCancel: () => void);
|
|
11
|
-
|
|
21
|
+
private filterProviders;
|
|
22
|
+
private updateList;
|
|
23
|
+
handleInput(keyData: string): void;
|
|
24
|
+
getSearchInput(): Input;
|
|
12
25
|
}
|
|
@@ -1,43 +1,150 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* [WHO]: ProviderSelectorComponent
|
|
3
3
|
* [FROM]: Depends on @pencil-agent/tui, ../theme/theme.js, ./dynamic-border.js
|
|
4
|
-
* [TO]: Consumed by modes/interactive/
|
|
4
|
+
* [TO]: Consumed by modes/interactive/interactive-mode.ts
|
|
5
5
|
* [HERE]: modes/interactive/components/provider-selector.ts -
|
|
6
|
+
* Adds search to provider selection when /model has many providers
|
|
6
7
|
*/
|
|
7
|
-
import { Container,
|
|
8
|
+
import { Container, fuzzyFilter, getEditorKeybindings, Input, Spacer, Text, } from "@pencil-agent/tui";
|
|
8
9
|
import { getCustomProtocolProviderDefinition, isCustomProtocolProvider, } from "../../../core/custom-providers.js";
|
|
9
|
-
import {
|
|
10
|
+
import { theme } from "../theme/theme.js";
|
|
10
11
|
import { DynamicBorder } from "./dynamic-border.js";
|
|
11
12
|
export class ProviderSelectorComponent extends Container {
|
|
12
|
-
|
|
13
|
+
searchInput;
|
|
14
|
+
listContainer;
|
|
15
|
+
allProviders;
|
|
16
|
+
filteredProviders;
|
|
17
|
+
selectedIndex = 0;
|
|
18
|
+
onSelectCallback;
|
|
19
|
+
onCancelCallback;
|
|
20
|
+
_focused = false;
|
|
21
|
+
get focused() {
|
|
22
|
+
return this._focused;
|
|
23
|
+
}
|
|
24
|
+
set focused(value) {
|
|
25
|
+
this._focused = value;
|
|
26
|
+
this.searchInput.focused = value;
|
|
27
|
+
}
|
|
13
28
|
constructor(providers, currentProvider, onSelect, onCancel) {
|
|
14
29
|
super();
|
|
15
|
-
this.
|
|
16
|
-
|
|
30
|
+
this.onSelectCallback = onSelect;
|
|
31
|
+
this.onCancelCallback = onCancel;
|
|
32
|
+
// Build provider items with custom protocol labels
|
|
33
|
+
this.allProviders = providers.map((provider) => {
|
|
17
34
|
const customProvider = isCustomProtocolProvider(provider)
|
|
18
35
|
? getCustomProtocolProviderDefinition(provider)
|
|
19
36
|
: undefined;
|
|
20
37
|
return {
|
|
21
38
|
value: provider,
|
|
22
39
|
label: customProvider?.label ?? provider,
|
|
23
|
-
description:
|
|
24
|
-
|
|
25
|
-
? "(current, press Enter to edit)"
|
|
26
|
-
: "(current)"
|
|
27
|
-
: customProvider?.description,
|
|
40
|
+
description: customProvider?.description,
|
|
41
|
+
isCurrent: provider === currentProvider,
|
|
28
42
|
};
|
|
29
43
|
});
|
|
30
|
-
this.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
this.filteredProviders = [...this.allProviders];
|
|
45
|
+
// Set initial selection to current provider
|
|
46
|
+
if (currentProvider) {
|
|
47
|
+
const currentIdx = this.allProviders.findIndex((p) => p.value === currentProvider);
|
|
48
|
+
if (currentIdx >= 0) {
|
|
49
|
+
this.selectedIndex = currentIdx;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
this.addChild(new DynamicBorder());
|
|
53
|
+
this.addChild(new Spacer(1));
|
|
54
|
+
this.addChild(new Text(theme.fg("muted", "Search provider:"), 0, 0));
|
|
55
|
+
this.addChild(new Spacer(1));
|
|
56
|
+
this.searchInput = new Input();
|
|
57
|
+
this.searchInput.onSubmit = () => {
|
|
58
|
+
if (this.filteredProviders[this.selectedIndex]) {
|
|
59
|
+
this.onSelectCallback(this.filteredProviders[this.selectedIndex].value);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
this.addChild(this.searchInput);
|
|
63
|
+
this.addChild(new Spacer(1));
|
|
64
|
+
this.listContainer = new Container();
|
|
65
|
+
this.addChild(this.listContainer);
|
|
66
|
+
this.addChild(new Spacer(1));
|
|
38
67
|
this.addChild(new DynamicBorder());
|
|
68
|
+
this.updateList();
|
|
69
|
+
}
|
|
70
|
+
filterProviders(query) {
|
|
71
|
+
const filtered = query
|
|
72
|
+
? fuzzyFilter(this.allProviders, query, (p) => `${p.label} ${p.value}`)
|
|
73
|
+
: [...this.allProviders];
|
|
74
|
+
this.filteredProviders = filtered;
|
|
75
|
+
// Reset selectedIndex to first item when filter changes
|
|
76
|
+
this.selectedIndex = 0;
|
|
77
|
+
this.updateList();
|
|
78
|
+
}
|
|
79
|
+
updateList() {
|
|
80
|
+
// Remove all old children
|
|
81
|
+
while (this.listContainer.children.length > 0) {
|
|
82
|
+
this.listContainer.children.pop();
|
|
83
|
+
}
|
|
84
|
+
const maxVisible = 10;
|
|
85
|
+
const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(maxVisible / 2), this.filteredProviders.length - maxVisible));
|
|
86
|
+
const endIndex = Math.min(startIndex + maxVisible, this.filteredProviders.length);
|
|
87
|
+
for (let index = startIndex; index < endIndex; index++) {
|
|
88
|
+
const item = this.filteredProviders[index];
|
|
89
|
+
if (!item)
|
|
90
|
+
continue;
|
|
91
|
+
const isSelected = index === this.selectedIndex;
|
|
92
|
+
const prefix = isSelected
|
|
93
|
+
? theme.fg("accent", "->")
|
|
94
|
+
: " ";
|
|
95
|
+
const labelText = isSelected
|
|
96
|
+
? theme.fg("accent", item.label)
|
|
97
|
+
: item.label;
|
|
98
|
+
const currentTag = item.isCurrent
|
|
99
|
+
? theme.fg("success", " [current]")
|
|
100
|
+
: "";
|
|
101
|
+
const descText = item.description
|
|
102
|
+
? theme.fg("muted", ` ${item.description}`)
|
|
103
|
+
: "";
|
|
104
|
+
this.listContainer.addChild(new Text(`${prefix}${labelText}${currentTag}${descText}`, 0, 0));
|
|
105
|
+
}
|
|
106
|
+
if (startIndex > 0 || endIndex < this.filteredProviders.length) {
|
|
107
|
+
this.listContainer.addChild(new Text(theme.fg("muted", ` (${this.selectedIndex + 1}/${this.filteredProviders.length})`), 0, 0));
|
|
108
|
+
}
|
|
109
|
+
if (this.filteredProviders.length === 0) {
|
|
110
|
+
this.listContainer.addChild(new Text(theme.fg("muted", " No matching providers"), 0, 0));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
handleInput(keyData) {
|
|
114
|
+
const kb = getEditorKeybindings();
|
|
115
|
+
if (kb.matches(keyData, "selectUp")) {
|
|
116
|
+
if (this.filteredProviders.length === 0)
|
|
117
|
+
return;
|
|
118
|
+
this.selectedIndex =
|
|
119
|
+
this.selectedIndex === 0
|
|
120
|
+
? this.filteredProviders.length - 1
|
|
121
|
+
: this.selectedIndex - 1;
|
|
122
|
+
this.updateList();
|
|
123
|
+
}
|
|
124
|
+
else if (kb.matches(keyData, "selectDown")) {
|
|
125
|
+
if (this.filteredProviders.length === 0)
|
|
126
|
+
return;
|
|
127
|
+
this.selectedIndex =
|
|
128
|
+
this.selectedIndex === this.filteredProviders.length - 1
|
|
129
|
+
? 0
|
|
130
|
+
: this.selectedIndex + 1;
|
|
131
|
+
this.updateList();
|
|
132
|
+
}
|
|
133
|
+
else if (kb.matches(keyData, "selectConfirm")) {
|
|
134
|
+
const selected = this.filteredProviders[this.selectedIndex];
|
|
135
|
+
if (selected) {
|
|
136
|
+
this.onSelectCallback(selected.value);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else if (kb.matches(keyData, "selectCancel")) {
|
|
140
|
+
this.onCancelCallback();
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
this.searchInput.handleInput(keyData);
|
|
144
|
+
this.filterProviders(this.searchInput.getValue());
|
|
145
|
+
}
|
|
39
146
|
}
|
|
40
|
-
|
|
41
|
-
return this.
|
|
147
|
+
getSearchInput() {
|
|
148
|
+
return this.searchInput;
|
|
42
149
|
}
|
|
43
150
|
}
|
|
@@ -41,7 +41,7 @@ import { DynamicBorder } from "./components/dynamic-border.js";
|
|
|
41
41
|
import { ExtensionEditorComponent } from "./components/extension-editor.js";
|
|
42
42
|
import { ExtensionInputComponent } from "./components/extension-input.js";
|
|
43
43
|
import { ExtensionSelectorComponent } from "./components/extension-selector.js";
|
|
44
|
-
import { FooterComponent } from "./components/footer.js";
|
|
44
|
+
import { FooterComponent, renderContextProgressBar } from "./components/footer.js";
|
|
45
45
|
import { appKey, appKeyHint, editorKey, keyHint, rawKeyHint, } from "./components/keybinding-hints.js";
|
|
46
46
|
import { LoginDialogComponent } from "./components/login-dialog.js";
|
|
47
47
|
import { ModelSelectorComponent } from "./components/model-selector.js";
|
|
@@ -3740,7 +3740,7 @@ export class InteractiveMode {
|
|
|
3740
3740
|
done();
|
|
3741
3741
|
this.ui.requestRender();
|
|
3742
3742
|
});
|
|
3743
|
-
return { component: selector, focus: selector
|
|
3743
|
+
return { component: selector, focus: selector };
|
|
3744
3744
|
});
|
|
3745
3745
|
return;
|
|
3746
3746
|
}
|
|
@@ -4004,7 +4004,7 @@ export class InteractiveMode {
|
|
|
4004
4004
|
done();
|
|
4005
4005
|
this.ui.requestRender();
|
|
4006
4006
|
});
|
|
4007
|
-
return { component: selector, focus: selector
|
|
4007
|
+
return { component: selector, focus: selector };
|
|
4008
4008
|
});
|
|
4009
4009
|
}
|
|
4010
4010
|
async showModelsSelector() {
|
|
@@ -4716,12 +4716,7 @@ export class InteractiveMode {
|
|
|
4716
4716
|
const contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;
|
|
4717
4717
|
const contextPercent = contextUsage?.percent ?? 0;
|
|
4718
4718
|
const contextTokens = contextUsage?.tokens ?? 0;
|
|
4719
|
-
|
|
4720
|
-
const barWidth = 12;
|
|
4721
|
-
const filled = Math.round((contextPercent / 100) * barWidth);
|
|
4722
|
-
const empty = barWidth - filled;
|
|
4723
|
-
const fillColor = contextPercent > 90 ? "error" : contextPercent > 70 ? "warning" : "success";
|
|
4724
|
-
const bar = theme.fg("dim", "[") + theme.fg(fillColor, "█".repeat(filled)) + theme.fg("dim", "░".repeat(empty)) + theme.fg("dim", "]");
|
|
4719
|
+
const bar = renderContextProgressBar(contextPercent);
|
|
4725
4720
|
const contextLine = ` Context: ${bar} ${contextPercent.toFixed(1)}% (${fmt(contextTokens)}/${fmt(contextWindow)})`;
|
|
4726
4721
|
lines.push(theme.fg("border", `│`) + padLine(contextLine, width) + theme.fg("border", `│`));
|
|
4727
4722
|
// Bottom border
|
|
File without changes
|