@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.
Files changed (2) hide show
  1. package/components.ts +187 -0
  2. 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.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",