@fanchaozz/provider-manager 1.0.0 → 1.0.1
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/components.ts +187 -0
- package/package.json +1 -1
package/components.ts
CHANGED
|
@@ -35,6 +35,77 @@ export function matchesKey(data: string, key: string): boolean {
|
|
|
35
35
|
return false;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Bracketed-paste 助手
|
|
40
|
+
// =========================================================================
|
|
41
|
+
//
|
|
42
|
+
// pi 框架的 Ctrl+V / Alt+V / 右键粘贴都走 readClipboardText(),拿到的文本
|
|
43
|
+
// 用 ESC[200~ ... ESC[201~ 包后调 formEditor.handleInput。我们之前的
|
|
44
|
+
// handleInput 只认 “单字节 + charCode 32-126”,遇到这个多字节序列会
|
|
45
|
+
// 静默丢,与 “pwsh 下能选中文/多行 key 但 ctrl+v 无反应” 症状一致。
|
|
46
|
+
//
|
|
47
|
+
// Feed 每次输入 chunk;返回 true = 本 chunk 被粘贴状态完全消费(调用方退出)。
|
|
48
|
+
// 返回 false = 不是粘贴;调用方走常规路径。
|
|
49
|
+
// 跨多次 feed() 累积;遇到结束标记时调一次 onPaste(干净文本) 并清状态。
|
|
50
|
+
// 当本 chunk 结束后还有剩余字符(同一 chunk 里在结束标记后还有正常输入),
|
|
51
|
+
// 存到 `lastRest`,调用方可用 `consumeRest()` 拿到并重调。
|
|
52
|
+
export class PasteBuffer {
|
|
53
|
+
private isInPaste = false;
|
|
54
|
+
private buf = "";
|
|
55
|
+
private lastRest = "";
|
|
56
|
+
/** Called once when a complete bracketed paste arrives. */
|
|
57
|
+
onPaste: (cleanText: string) => void = () => {};
|
|
58
|
+
|
|
59
|
+
feed(data: string): boolean {
|
|
60
|
+
// 粘贴中:累积到 buf 等结束标记
|
|
61
|
+
if (this.isInPaste) {
|
|
62
|
+
this.buf += data;
|
|
63
|
+
const end = this.buf.indexOf("\x1b[201~");
|
|
64
|
+
if (end >= 0) {
|
|
65
|
+
const text = this.buf.substring(0, end);
|
|
66
|
+
const rest = this.buf.substring(end + 6);
|
|
67
|
+
this.isInPaste = false;
|
|
68
|
+
this.buf = "";
|
|
69
|
+
this.onPaste(sanitizePasteText(text));
|
|
70
|
+
if (rest) this.lastRest = rest;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
// 未粘贴:检测开始标记
|
|
75
|
+
if (data.includes("\x1b[200~")) {
|
|
76
|
+
this.isInPaste = true;
|
|
77
|
+
this.buf = "";
|
|
78
|
+
const after = data.replace("\x1b[200~", "");
|
|
79
|
+
// 同一 chunk 里也可能有结束标记
|
|
80
|
+
const end = after.indexOf("\x1b[201~");
|
|
81
|
+
if (end >= 0) {
|
|
82
|
+
const text = after.substring(0, end);
|
|
83
|
+
const rest = after.substring(end + 6);
|
|
84
|
+
this.isInPaste = false;
|
|
85
|
+
this.onPaste(sanitizePasteText(text));
|
|
86
|
+
if (rest) this.lastRest = rest;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
this.buf = after;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return false; // 非粘贴,调用方走常规路径
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 取走上次 paste 之后的剩余字符(如果有)。调用后清空。 */
|
|
96
|
+
consumeRest(): string {
|
|
97
|
+
const r = this.lastRest;
|
|
98
|
+
this.lastRest = "";
|
|
99
|
+
return r;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 清理粘贴文本:去 \r\n / \r / \n;\t → 4 空格。
|
|
104
|
+
* 对齐 pi-tui Input.handlePaste 的行为。单行字段不需要换行。 */
|
|
105
|
+
function sanitizePasteText(s: string): string {
|
|
106
|
+
return s.replace(/\r\n/g, "").replace(/\r/g, "").replace(/\n/g, "").replace(/\t/g, " ");
|
|
107
|
+
}
|
|
108
|
+
|
|
38
109
|
// ============================================================================
|
|
39
110
|
// ModelChecklist — 多选 checklist(sync 用)
|
|
40
111
|
// ============================================================================
|
|
@@ -64,6 +135,8 @@ export class ModelChecklist {
|
|
|
64
135
|
private onCancel: () => void;
|
|
65
136
|
private cachedWidth = -1;
|
|
66
137
|
private cachedLines: string[] = [];
|
|
138
|
+
/** 括包粘贴缓冲:pi 框架的 Ctrl+V / Alt+V / 右键粘贴会用 ESC[200~..ESC[201~ 包后 call handleInput。 */
|
|
139
|
+
private pasteBuf = new PasteBuffer();
|
|
67
140
|
/** search 输入框的当前内容。同步起作用、可被全打印字符 / Backspace 修改。 */
|
|
68
141
|
private query = "";
|
|
69
142
|
|
|
@@ -89,6 +162,15 @@ export class ModelChecklist {
|
|
|
89
162
|
if (opts.preSelect && !opts.preSelect(it)) continue;
|
|
90
163
|
this.selected.add(it.id);
|
|
91
164
|
}
|
|
165
|
+
// 粘贴多字符进 search 输入框(如粘贴 apiKey / model id 过滤)
|
|
166
|
+
this.pasteBuf.onPaste = (text) => {
|
|
167
|
+
if (!text) return;
|
|
168
|
+
this.query += text;
|
|
169
|
+
const v = this.visibleItems();
|
|
170
|
+
if (this.cursor >= v.length) this.cursor = Math.max(0, v.length - 1);
|
|
171
|
+
this.top = 0;
|
|
172
|
+
this.invalidate();
|
|
173
|
+
};
|
|
92
174
|
}
|
|
93
175
|
|
|
94
176
|
/** query 作用后的可见项列表。空 query =全量;disabled 始终隐藏。 */
|
|
@@ -112,6 +194,12 @@ export class ModelChecklist {
|
|
|
112
194
|
}
|
|
113
195
|
|
|
114
196
|
handleInput(data: string): void {
|
|
197
|
+
// 括包粘贴:Ctrl+V / Alt+V / 右键粘走 PasteBuffer
|
|
198
|
+
if (this.pasteBuf.feed(data)) {
|
|
199
|
+
const rest = this.pasteBuf.consumeRest();
|
|
200
|
+
if (rest) this.processRest(rest);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
115
203
|
if (matchesKey(data, "escape") || data === "q") {
|
|
116
204
|
this.onCancel();
|
|
117
205
|
return;
|
|
@@ -183,6 +271,46 @@ export class ModelChecklist {
|
|
|
183
271
|
}
|
|
184
272
|
}
|
|
185
273
|
|
|
274
|
+
/** 处理括包粘贴后剩余的字符串:按字符逐个调 handleInput(不走 PasteBuffer 路径)。 */
|
|
275
|
+
private processRest(rest: string): void {
|
|
276
|
+
for (const ch of rest) {
|
|
277
|
+
// 直接走常规路径。上面 handleInput 的 pasteBuf.feed 在这个深度返回 false(同一实例状态不嵌套)
|
|
278
|
+
this.dispatchChar(ch);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** 单个字符的常规处理(从 handleInput 拆出,给 processRest 复用)。
|
|
283
|
+
* 只处理 “单可打印字符” / Space / Backspace;其他键(多字节转义序列、Enter 等)不响应。 */
|
|
284
|
+
private dispatchChar(ch: string): void {
|
|
285
|
+
if (matchesKey(ch, "backspace")) {
|
|
286
|
+
if (this.query.length > 0) {
|
|
287
|
+
this.query = this.query.slice(0, -1);
|
|
288
|
+
const v2 = this.visibleItems();
|
|
289
|
+
if (this.cursor >= v2.length) this.cursor = Math.max(0, v2.length - 1);
|
|
290
|
+
this.top = 0;
|
|
291
|
+
this.invalidate();
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (ch === " ") {
|
|
296
|
+
const visible = this.visibleItems();
|
|
297
|
+
const it = visible[this.cursor];
|
|
298
|
+
if (it && !it.disabled) {
|
|
299
|
+
if (this.selected.has(it.id)) this.selected.delete(it.id);
|
|
300
|
+
else this.selected.add(it.id);
|
|
301
|
+
this.invalidate();
|
|
302
|
+
}
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (ch.length === 1 && ch.charCodeAt(0) >= 32 && ch.charCodeAt(0) < 127) {
|
|
306
|
+
this.query += ch;
|
|
307
|
+
const v2 = this.visibleItems();
|
|
308
|
+
if (this.cursor >= v2.length) this.cursor = Math.max(0, v2.length - 1);
|
|
309
|
+
this.top = 0;
|
|
310
|
+
this.invalidate();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
186
314
|
invalidate(): void { this.cachedWidth = -1; this.cachedLines = []; }
|
|
187
315
|
|
|
188
316
|
render(width: number): string[] {
|
|
@@ -384,6 +512,8 @@ export class FormEditor<T extends Record<string, unknown>> {
|
|
|
384
512
|
private onCancel: () => void;
|
|
385
513
|
private cachedWidth = -1;
|
|
386
514
|
private cachedLines: string[] = [];
|
|
515
|
+
/** 括包粘贴缓冲:pi 框架的 Ctrl+V / Alt+V / 右键粘贴会用 ESC[200~..ESC[201~ 包后 call handleInput。 */
|
|
516
|
+
private pasteBuf = new PasteBuffer();
|
|
387
517
|
|
|
388
518
|
constructor(opts: {
|
|
389
519
|
title: string;
|
|
@@ -402,6 +532,29 @@ export class FormEditor<T extends Record<string, unknown>> {
|
|
|
402
532
|
this.onCancel = opts.onCancel;
|
|
403
533
|
this.draft = this.currentValueAsString();
|
|
404
534
|
this.draftIsOriginal = true;
|
|
535
|
+
// 粘贴:view 模式自动进 edit(避免被 view 模式接货的选中不动);仅 typeable 字段响应
|
|
536
|
+
this.pasteBuf.onPaste = (text) => {
|
|
537
|
+
if (!text) return;
|
|
538
|
+
const f0 = this.fields[this.cursor];
|
|
539
|
+
const isTypeable = f0 && (f0.type === "text" || f0.type === "secret" || f0.type === "number" || f0.type === "json");
|
|
540
|
+
if (!isTypeable) return;
|
|
541
|
+
if (!this.editing) {
|
|
542
|
+
// secret:跟 Enter 走同路径——草稿重置为原值,未修改标记
|
|
543
|
+
if (f0.type === "secret") {
|
|
544
|
+
const orig = (this.values as any)[f0.key];
|
|
545
|
+
this.draft = (orig === undefined || orig === null) ? "" : String(orig);
|
|
546
|
+
this.draftIsOriginal = true;
|
|
547
|
+
}
|
|
548
|
+
this.editing = true;
|
|
549
|
+
}
|
|
550
|
+
// number:过滤非数字,避免误粘 "123abc";secret / text / json 原样拼
|
|
551
|
+
const chunk = f0.type === "number" ? text.replace(/[^0-9]/g, "") : text;
|
|
552
|
+
if (!chunk) return;
|
|
553
|
+
if (f0.type === "number" && this.draftIsOriginal) this.draft = chunk; // 首次输入覆盖
|
|
554
|
+
else this.draft += chunk;
|
|
555
|
+
this.draftIsOriginal = false;
|
|
556
|
+
this.invalidate();
|
|
557
|
+
};
|
|
405
558
|
}
|
|
406
559
|
|
|
407
560
|
private currentValueAsString(): string {
|
|
@@ -519,6 +672,12 @@ export class FormEditor<T extends Record<string, unknown>> {
|
|
|
519
672
|
}
|
|
520
673
|
|
|
521
674
|
handleInput(data: string): void {
|
|
675
|
+
// 括包粘贴:Ctrl+V / Alt+V / 右键粘走 PasteBuffer(onPaste 检查 editing + typeable)
|
|
676
|
+
if (this.pasteBuf.feed(data)) {
|
|
677
|
+
const rest = this.pasteBuf.consumeRest();
|
|
678
|
+
if (rest) this.processRest(rest);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
522
681
|
const f0 = this.fields[this.cursor];
|
|
523
682
|
const isNonInput = f0?.type === "select" || f0?.type === "levelmap" || f0?.type === "multiselect";
|
|
524
683
|
const isTypeable = f0 && (f0.type === "text" || f0.type === "secret" || f0.type === "number" || f0.type === "json");
|
|
@@ -693,6 +852,34 @@ export class FormEditor<T extends Record<string, unknown>> {
|
|
|
693
852
|
}
|
|
694
853
|
}
|
|
695
854
|
|
|
855
|
+
/** 处理括包粘贴后剩余的字符串:按字符逐个调 handleInput(不走 PasteBuffer 路径)。 */
|
|
856
|
+
private processRest(rest: string): void {
|
|
857
|
+
for (const ch of rest) this.dispatchChar(ch);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** 单个字符的常规处理(仅 typeable 字符 / Backspace)。其他键不响应。 */
|
|
861
|
+
private dispatchChar(ch: string): void {
|
|
862
|
+
const f0 = this.fields[this.cursor];
|
|
863
|
+
const isTypeable = f0 && (f0.type === "text" || f0.type === "secret" || f0.type === "number" || f0.type === "json");
|
|
864
|
+
if (matchesKey(ch, "backspace")) {
|
|
865
|
+
if (!this.editing || !isTypeable) return;
|
|
866
|
+
if (this.draft.length > 0) this.draft = this.draft.slice(0, -1);
|
|
867
|
+
this.draftIsOriginal = false;
|
|
868
|
+
this.invalidate();
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (!this.editing || !isTypeable) return;
|
|
872
|
+
if (ch.length === 1 && ch.charCodeAt(0) >= 32 && ch.charCodeAt(0) < 127) {
|
|
873
|
+
if (f0?.type === "number" && /^\d$/.test(ch) && this.draftIsOriginal) {
|
|
874
|
+
this.draft = ch;
|
|
875
|
+
} else {
|
|
876
|
+
this.draft += ch;
|
|
877
|
+
}
|
|
878
|
+
this.draftIsOriginal = false;
|
|
879
|
+
this.invalidate();
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
696
883
|
invalidate(): void { this.cachedWidth = -1; this.cachedLines = []; }
|
|
697
884
|
|
|
698
885
|
private formatValue(f: FormField, v: unknown): string {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fanchaozz/provider-manager",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "A pi extension that manages custom providers and models in ~/.pi/agent/models.json via a TUI dashboard, /providers slash command, and remote model sync.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|