pi-web-ui 0.60.0 → 0.61.0

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.
@@ -129,10 +129,144 @@ export class ModelAdminService {
129
129
  constructor(host) {
130
130
  this.host = host;
131
131
  }
132
- /** Persist an api-key credential for a provider (auth.json) and apply it now. */
132
+ // ---------------------------------------------------------------------------
133
+ // Built-in provider multiple key store (one provider, several API keys).
134
+ // Persisted as <agentDir>/provider-keys.json:
135
+ // { "<providerId>": { activeKeyName: string|null, keys: [{name,apiKey}] } }
136
+ // The frontend only ever sees NAMES (no value, no masked fragment). The key
137
+ // value travels to the server ONCE on add and is stored (like auth.json); the
138
+ // server resolves + switches the active key by NAME.
139
+ // ---------------------------------------------------------------------------
140
+ providerKeysPath() {
141
+ return join(this.host.agentDir, "provider-keys.json");
142
+ }
143
+ /** Read + parse provider-keys.json. */
144
+ readProviderKeys() {
145
+ try {
146
+ const parsed = JSON.parse(readFileSync(this.providerKeysPath(), "utf8"));
147
+ const out = {};
148
+ for (const [pid, entry] of Object.entries(parsed)) {
149
+ const keys = Array.isArray(entry?.keys)
150
+ ? entry.keys.filter((k) => k?.name && k?.apiKey)
151
+ : [];
152
+ if (!pid || keys.length === 0)
153
+ continue;
154
+ const activeKeyName = entry.activeKeyName && keys.some((k) => k.name === entry.activeKeyName)
155
+ ? entry.activeKeyName
156
+ : keys[0].name;
157
+ out[pid] = { activeKeyName, keys };
158
+ }
159
+ return out;
160
+ }
161
+ catch {
162
+ return {};
163
+ }
164
+ }
165
+ writeProviderKeys(data) {
166
+ mkdirSync(this.host.agentDir, { recursive: true });
167
+ writeFileSync(this.providerKeysPath(), JSON.stringify(data, null, 2) + "\n");
168
+ }
169
+ /** Default name "密钥 N" for a provider's Nth key. */
170
+ defaultKeyName(keys) {
171
+ return `密钥 ${keys.length + 1}`;
172
+ }
173
+ /** Resolve a user-supplied (or default) name into a UNIQUE one (append
174
+ * " (2)", " (3)", … on collision) so a name is a reliable switch key. */
175
+ uniqueKeyName(entry, wanted) {
176
+ const base = (wanted?.trim() || this.defaultKeyName(entry.keys)).trim() || this.defaultKeyName(entry.keys);
177
+ const taken = new Set(entry.keys.map((k) => k.name));
178
+ let name = base;
179
+ let n = 2;
180
+ while (taken.has(name))
181
+ name = `${base} (${n++})`;
182
+ return name;
183
+ }
184
+ /** Build the name-only ProviderKeyInfo list for a provider (no value/mask). */
185
+ providerKeysInfo(data) {
186
+ const keys = {};
187
+ for (const [pid, entry] of Object.entries(data)) {
188
+ keys[pid] = entry.keys.map((k) => ({ name: k.name, active: entry.activeKeyName === k.name }));
189
+ }
190
+ return { keys };
191
+ }
192
+ /** Get the currently active key name for a provider, or null. */
193
+ getActiveKeyName(provider) {
194
+ const data = this.readProviderKeys();
195
+ return data[provider]?.activeKeyName ?? null;
196
+ }
197
+ /** Seed a provider's key list from an EXISTING auth.json credential (legacy
198
+ * configs written before the multi-key store existed) so the store stays
199
+ * authoritative and the UI shows the current active key immediately even
200
+ * before the user adds a second key. Idempotent — does nothing if the
201
+ * provider already has a store entry. */
202
+ seedProviderKeysFromAuth(pid, data) {
203
+ if (data[pid])
204
+ return;
205
+ try {
206
+ const auth = JSON.parse(readFileSync(join(this.host.agentDir, "auth.json"), "utf8"));
207
+ const cred = auth[pid];
208
+ if (cred && typeof cred.key === "string" && cred.key.trim()) {
209
+ data[pid] = {
210
+ activeKeyName: "密钥 1",
211
+ keys: [{ name: "密钥 1", apiKey: cred.key.trim() }],
212
+ };
213
+ }
214
+ }
215
+ catch {
216
+ // no auth.json / unparsable — nothing to seed
217
+ }
218
+ }
219
+ /** Push the masked provider-keys map to the client. Seeds the store from any
220
+ * auth.json credentials so legacy single-key setups show up immediately. */
221
+ listProviderKeys() {
222
+ const data = this.readProviderKeys();
223
+ for (const pid of this.builtinProviderIds())
224
+ this.seedProviderKeysFromAuth(pid, data);
225
+ this.writeProviderKeys(data);
226
+ this.host.emit({ type: "provider_keys", ...this.providerKeysInfo(data) });
227
+ this.host.flushSnapshot();
228
+ }
229
+ /** Candidate built-in provider ids whose keys we track: those with a store
230
+ * entry plus every provider actually registered in the runtime (seed reads
231
+ * auth.json per id, so only real providers with a credential get seeded —
232
+ * unrelated auth.json entries like "main" are ignored). */
233
+ builtinProviderIds() {
234
+ const data = this.readProviderKeys();
235
+ const ids = new Set(Object.keys(data));
236
+ try {
237
+ for (const p of this.host.modelRuntime().getProviders())
238
+ ids.add(p.id);
239
+ }
240
+ catch {
241
+ // runtime not ready
242
+ }
243
+ return [...ids];
244
+ }
245
+ /** Persist the ACTIVE key's apiKey into auth.json + runtime override + refresh. */
246
+ async applyActiveKey(pid, apiKey) {
247
+ const authPath = join(this.host.agentDir, "auth.json");
248
+ mkdirSync(this.host.agentDir, { recursive: true });
249
+ let data = {};
250
+ try {
251
+ data = JSON.parse(readFileSync(authPath, "utf8"));
252
+ }
253
+ catch {
254
+ // no file yet / unparsable — start fresh
255
+ }
256
+ data[pid] = { type: "api_key", key: apiKey };
257
+ writeFileSync(authPath, JSON.stringify(data, null, 2) + "\n");
258
+ const mr = this.host.modelRuntime();
259
+ await mr.setRuntimeApiKey(pid, apiKey);
260
+ await mr.refresh({ allowNetwork: true, providers: [pid] });
261
+ this.host.invalidatePiConfig();
262
+ }
263
+ /** Persist an api-key credential for a provider (auth.json) and apply it now.
264
+ * Also records the key in provider-keys.json (as the active key), so it shows
265
+ * in the multi-key list too. */
133
266
  async setProviderApiKey(provider, apiKey) {
267
+ const pid = provider.trim();
134
268
  const key = apiKey.trim();
135
- if (!provider.trim()) {
269
+ if (!pid) {
136
270
  this.host.emit({ type: "notice", level: "error", text: "请填写服务商 ID" });
137
271
  return;
138
272
  }
@@ -141,32 +275,36 @@ export class ModelAdminService {
141
275
  return;
142
276
  }
143
277
  try {
144
- // Persist to auth.json (auth.json shape: { <provider>: { type: "api_key", key } }).
145
- const authPath = join(this.host.agentDir, "auth.json");
146
- mkdirSync(this.host.agentDir, { recursive: true });
147
- let data = {};
148
- try {
149
- data = JSON.parse(readFileSync(authPath, "utf8"));
278
+ const data = this.readProviderKeys();
279
+ // Preserve a legacy auth.json key as the first (active) entry so adding
280
+ // a new key stacks alongside it instead of clobbering it.
281
+ if (!data[pid])
282
+ this.seedProviderKeysFromAuth(pid, data);
283
+ let entry = data[pid];
284
+ if (!entry)
285
+ entry = data[pid] = { activeKeyName: null, keys: [] };
286
+ const existing = entry.keys.find((k) => k.apiKey === key);
287
+ let name;
288
+ if (existing) {
289
+ // Same key value already in the list → just make it active.
290
+ entry.activeKeyName = existing.name;
291
+ name = existing.name;
150
292
  }
151
- catch {
152
- // no file yet / unparsable — start fresh
293
+ else {
294
+ name = this.uniqueKeyName(entry, undefined);
295
+ entry.keys.push({ name, apiKey: key });
296
+ entry.activeKeyName = name;
153
297
  }
154
- data[provider.trim()] = { type: "api_key", key };
155
- writeFileSync(authPath, JSON.stringify(data, null, 2) + "\n");
156
- // Apply immediately for this session (runtime credentials are cached), then
157
- // refresh models. allowNetwork downloads the provider's official model
158
- // catalog (openai/anthropic/… are dynamic providers with no built-in list).
159
- const mr = this.host.modelRuntime();
160
- await mr.setRuntimeApiKey(provider.trim(), key);
161
- await mr.refresh({ allowNetwork: true });
162
- this.host.invalidatePiConfig();
298
+ this.writeProviderKeys(data);
299
+ await this.applyActiveKey(pid, key);
163
300
  this.host.emit({
164
301
  type: "notice",
165
302
  level: "info",
166
- text: `✅ 已保存 ${provider.trim()} 的 API 密钥并刷新模型列表`,
303
+ text: `✅ 已保存 ${pid} 的密钥「${name}」并刷新模型列表`,
167
304
  });
168
305
  await this.host.pushModels();
169
306
  await this.listProviders();
307
+ this.listProviderKeys();
170
308
  }
171
309
  catch (err) {
172
310
  this.host.emit({
@@ -177,6 +315,181 @@ export class ModelAdminService {
177
315
  }
178
316
  this.host.flushSnapshot();
179
317
  }
318
+ /** Add a SECONDARY API key to a built-in provider's key list. `name` is the
319
+ * only thing the frontend ever sees (auto-generated when blank, deduped on
320
+ * collision). The added key stays INACTIVE unless it is the provider's first
321
+ * key; the user switches to it by name or by clicking a model under it. */
322
+ async addProviderKey(provider, apiKey, name) {
323
+ const pid = provider.trim();
324
+ const key = apiKey.trim();
325
+ if (!pid) {
326
+ this.host.emit({ type: "notice", level: "error", text: "请填写服务商 ID" });
327
+ return;
328
+ }
329
+ if (!key) {
330
+ this.host.emit({ type: "notice", level: "error", text: "请填写 API 密钥" });
331
+ return;
332
+ }
333
+ try {
334
+ const data = this.readProviderKeys();
335
+ // Preserve a legacy auth.json key (active) so the new key stacks as a
336
+ // SECONDARY inactive key rather than replacing the current one.
337
+ if (!data[pid])
338
+ this.seedProviderKeysFromAuth(pid, data);
339
+ let entry = data[pid];
340
+ if (!entry)
341
+ entry = data[pid] = { activeKeyName: null, keys: [] };
342
+ const dup = entry.keys.find((k) => k.apiKey === key);
343
+ if (dup) {
344
+ this.host.emit({
345
+ type: "notice",
346
+ level: "info",
347
+ text: `${pid} 已存在该密钥`,
348
+ });
349
+ return;
350
+ }
351
+ const keyName = this.uniqueKeyName(entry, name);
352
+ entry.keys.push({ name: keyName, apiKey: key });
353
+ // First key becomes active (provider had none usable yet).
354
+ if (!entry.activeKeyName)
355
+ entry.activeKeyName = keyName;
356
+ this.writeProviderKeys(data);
357
+ const isActive = entry.activeKeyName === keyName;
358
+ if (isActive) {
359
+ await this.applyActiveKey(pid, key);
360
+ this.host.emit({
361
+ type: "notice",
362
+ level: "info",
363
+ text: `🔑 已添加 ${pid} 的密钥「${keyName}」并设为当前`,
364
+ });
365
+ }
366
+ else {
367
+ this.host.emit({
368
+ type: "notice",
369
+ level: "info",
370
+ text: `🔑 已添加 ${pid} 的密钥「${keyName}」,点击模型时可切换使用`,
371
+ });
372
+ }
373
+ await this.host.pushModels();
374
+ await this.listProviders();
375
+ this.listProviderKeys();
376
+ }
377
+ catch (err) {
378
+ this.host.emit({
379
+ type: "notice",
380
+ level: "error",
381
+ text: `添加密钥失败:${err.message}`,
382
+ });
383
+ }
384
+ this.host.flushSnapshot();
385
+ }
386
+ /** Make a stored API key the ACTIVE one for a built-in provider by NAME (the
387
+ * server resolves the stored value from the name). */
388
+ async activateProviderKey(provider, keyName) {
389
+ const pid = provider.trim();
390
+ const targetName = keyName.trim();
391
+ try {
392
+ const data = this.readProviderKeys();
393
+ const entry = data[pid];
394
+ const target = entry?.keys.find((k) => k.name === targetName);
395
+ if (!target) {
396
+ this.host.emit({ type: "notice", level: "error", text: `${pid} 的密钥「${targetName}」不存在` });
397
+ return;
398
+ }
399
+ if (entry.activeKeyName === targetName) {
400
+ this.host.emit({ type: "notice", level: "info", text: `「${targetName}」已是当前密钥` });
401
+ return;
402
+ }
403
+ entry.activeKeyName = targetName;
404
+ this.writeProviderKeys(data);
405
+ await this.applyActiveKey(pid, target.apiKey);
406
+ this.host.emit({
407
+ type: "notice",
408
+ level: "info",
409
+ text: `⚡ 已切换到 ${pid} 的「${targetName}」`,
410
+ });
411
+ await this.host.pushModels();
412
+ await this.listProviders();
413
+ this.listProviderKeys();
414
+ }
415
+ catch (err) {
416
+ this.host.emit({
417
+ type: "notice",
418
+ level: "error",
419
+ text: `切换密钥失败:${err.message}`,
420
+ });
421
+ }
422
+ this.host.flushSnapshot();
423
+ }
424
+ /** Remove a stored API key by NAME. If it was active, the first remaining key
425
+ * becomes active (or the provider returns to unconfigured when no key is left). */
426
+ async removeProviderKey(provider, keyName) {
427
+ const pid = provider.trim();
428
+ const targetName = keyName.trim();
429
+ try {
430
+ const data = this.readProviderKeys();
431
+ const entry = data[pid];
432
+ if (!entry || !entry.keys.some((k) => k.name === targetName)) {
433
+ this.host.emit({ type: "notice", level: "error", text: `${pid} 的密钥「${targetName}」不存在` });
434
+ return;
435
+ }
436
+ const wasActive = entry.activeKeyName === targetName;
437
+ entry.keys = entry.keys.filter((k) => k.name !== targetName);
438
+ if (entry.keys.length === 0) {
439
+ delete data[pid];
440
+ this.writeProviderKeys(data);
441
+ // Drop auth.json entry + runtime override so the provider returns
442
+ // to unconfigured (its stored keys are gone too).
443
+ const authPath = join(this.host.agentDir, "auth.json");
444
+ let auth = {};
445
+ try {
446
+ auth = JSON.parse(readFileSync(authPath, "utf8"));
447
+ }
448
+ catch {
449
+ // no file yet — nothing to clean
450
+ }
451
+ delete auth[pid];
452
+ writeFileSync(authPath, JSON.stringify(auth, null, 2) + "\n");
453
+ const mr = this.host.modelRuntime();
454
+ await mr.removeRuntimeApiKey(pid);
455
+ await mr.refresh({ providers: [pid] });
456
+ this.host.invalidatePiConfig();
457
+ this.host.emit({
458
+ type: "notice",
459
+ level: "info",
460
+ text: `🗑 已移除 ${pid} 的密钥「${targetName}」,该服务商回到未配置状态`,
461
+ });
462
+ }
463
+ else {
464
+ if (wasActive) {
465
+ entry.activeKeyName = entry.keys[0].name;
466
+ this.writeProviderKeys(data);
467
+ await this.applyActiveKey(pid, entry.keys[0].apiKey);
468
+ }
469
+ else {
470
+ this.writeProviderKeys(data);
471
+ }
472
+ this.host.emit({
473
+ type: "notice",
474
+ level: "info",
475
+ text: wasActive
476
+ ? `🗑 已移除「${targetName}」,已切换到 ${entry.keys[0].name}`
477
+ : `🗑 已移除 ${pid} 的密钥「${targetName}」`,
478
+ });
479
+ }
480
+ await this.host.pushModels();
481
+ await this.listProviders();
482
+ this.listProviderKeys();
483
+ }
484
+ catch (err) {
485
+ this.host.emit({
486
+ type: "notice",
487
+ level: "error",
488
+ text: `移除密钥失败:${err.message}`,
489
+ });
490
+ }
491
+ this.host.flushSnapshot();
492
+ }
180
493
  /**
181
494
  * Clear a built-in provider's stored API key (auth.json entry + runtime
182
495
  * override) so it returns to the unconfigured state — its models disappear
@@ -200,7 +513,9 @@ export class ModelAdminService {
200
513
  catch {
201
514
  // no file yet / unparsable — nothing stored to clear
202
515
  }
203
- if (!(pid in data)) {
516
+ const keyData = this.readProviderKeys();
517
+ const hasStoredKeys = (keyData[pid]?.keys.length ?? 0) > 0;
518
+ if (!(pid in data) && !hasStoredKeys) {
204
519
  this.host.emit({
205
520
  type: "notice",
206
521
  level: "info",
@@ -210,11 +525,14 @@ export class ModelAdminService {
210
525
  }
211
526
  delete data[pid];
212
527
  writeFileSync(authPath, JSON.stringify(data, null, 2) + "\n");
528
+ // Clear every stored key so the provider returns to unconfigured.
529
+ delete keyData[pid];
530
+ this.writeProviderKeys(keyData);
213
531
  // Drop the runtime override too, then re-read credentials so the
214
532
  // provider goes back to unconfigured and its models leave the list.
215
533
  const mr = this.host.modelRuntime();
216
534
  await mr.removeRuntimeApiKey(pid);
217
- await mr.refresh();
535
+ await mr.refresh({ providers: [pid] });
218
536
  this.host.invalidatePiConfig();
219
537
  this.host.emit({
220
538
  type: "notice",
@@ -223,6 +541,7 @@ export class ModelAdminService {
223
541
  });
224
542
  await this.host.pushModels();
225
543
  await this.listProviders();
544
+ this.listProviderKeys();
226
545
  }
227
546
  catch (err) {
228
547
  this.host.emit({
@@ -243,7 +562,10 @@ export class ModelAdminService {
243
562
  */
244
563
  async cloneProvider(providerId, reqId) {
245
564
  const pid = providerId.trim();
246
- const fail = (error) => this.host.emit({ type: "clone_provider_result", reqId, ok: false, error });
565
+ const fail = (error) => {
566
+ this.host.emit({ type: "notice", level: "error", text: error });
567
+ this.host.emit({ type: "clone_provider_result", reqId, ok: false, error });
568
+ };
247
569
  try {
248
570
  if (!pid) {
249
571
  fail("请填写服务商 ID");
@@ -255,10 +577,7 @@ export class ModelAdminService {
255
577
  fail(`供应商 ${pid} 不存在`);
256
578
  return;
257
579
  }
258
- if (!p.baseUrl) {
259
- fail(`${pid} 没有 baseUrl(OAuth/环境变量型供应商),无法复制为自定义服务商`);
260
- return;
261
- }
580
+ const noBaseUrl = !p.baseUrl;
262
581
  // Map runtime models → models.json rows; dynamic providers ship an
263
582
  // empty catalog until refreshed over the network.
264
583
  const readModels = () => {
@@ -290,7 +609,8 @@ export class ModelAdminService {
290
609
  fail(`${pid} 的模型列表为空,无法复制(请稍后重试)`);
291
610
  return;
292
611
  }
293
- // models.json api provider 级:取占比最高的 api,只复制该 api 的模型。
612
+ // 供应商级 api 取占比最高,模型保留全量去重(避免 muse-spark 被过滤)
613
+ // 多 key 场景:复制一次即得到 opencode1/opencode2 两组,界面按供应商分组,选模型即切 key
294
614
  const counts = new Map();
295
615
  for (const m of models)
296
616
  counts.set(m.api, (counts.get(m.api) ?? 0) + 1);
@@ -298,9 +618,11 @@ export class ModelAdminService {
298
618
  for (const [k, v] of counts)
299
619
  if (v > (counts.get(api) ?? 0))
300
620
  api = k;
301
- const kept = models.filter((m) => m.api === api).map((m) => m.entry);
302
- // Suggest a free id (<pid>-2, -3, …) — save_model_config would silently
303
- // overwrite an existing custom entry with the same id.
621
+ const keptMap = new Map();
622
+ for (const m of models)
623
+ if (!keptMap.has(m.entry.id))
624
+ keptMap.set(m.entry.id, m.entry);
625
+ const kept = [...keptMap.values()].sort((a, b) => a.id.localeCompare(b.id));
304
626
  const taken = new Set([
305
627
  ...Object.keys(this.readModelsConfig().providers),
306
628
  ...mr.getRegisteredProviderIds(),
@@ -308,19 +630,22 @@ export class ModelAdminService {
308
630
  let newId = `${pid}-2`;
309
631
  for (let n = 2; taken.has(newId); n++)
310
632
  newId = `${pid}-${n}`;
633
+ const defaultBaseUrl = noBaseUrl && (pid === "opencode-go" || pid === "opencode") ? "http://127.0.0.1:4096" : undefined;
311
634
  const config = {
312
635
  providerId: newId,
313
636
  name: p.name,
314
637
  api,
315
- baseUrl: p.baseUrl,
638
+ ...(p.baseUrl ? { baseUrl: p.baseUrl } : defaultBaseUrl ? { baseUrl: defaultBaseUrl } : {}),
316
639
  models: kept,
317
640
  };
318
641
  this.host.emit({
319
642
  type: "notice",
320
- level: "info",
321
- text: `📋 已复制 ${pid} → ${newId}(${kept.length} 个模型),请填入新的 API 密钥后保存`,
643
+ level: noBaseUrl ? "warning" : "info",
644
+ text: noBaseUrl
645
+ ? `📋 已复制 ${pid} → ${newId}(${kept.length} 个模型),该供应商无远程 baseUrl,已生成模板请手动填写 baseUrl 和新的 API 密钥后保存`
646
+ : `📋 已复制 ${pid} → ${newId}(${kept.length} 个模型),请填入新的 API 密钥后保存`,
322
647
  });
323
- this.host.emit({ type: "clone_provider_result", reqId, ok: true, config });
648
+ this.host.emit({ type: "clone_provider_result", reqId, ok: true, config, configs: [config] });
324
649
  }
325
650
  catch (err) {
326
651
  fail(`复制服务商失败:${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-ui",
3
- "version": "0.60.0",
3
+ "version": "0.61.0",
4
4
  "description": "Web chat interface for the pi coding agent, powered by the pi SDK (@earendil-works/pi-coding-agent) — one-command run, Docker/systemd/launchd deployable",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -1,2 +1,2 @@
1
- import{a as c,j as s}from"./markdown-DRBrS2Nf.js";import{b as R,T as I,u as L,F as J,a as K,c as X,d as Q,e as U,f as V,g as W,h as Y,r as Z}from"./index-TZDcJFdi.js";import{D as ee,o as se}from"./xterm-D1D2FVe3.js";import"./react-C9ovnpIm.js";function ne({conversationId:n,terminalId:o,command:x,cwd:t,active:f,send:m,register:b}){const j=c.useRef(null),w=c.useRef(null),g=x?JSON.stringify(x):"";return c.useEffect(()=>{const p=j.current;if(!p)return;const a=new ee({theme:R(),fontFamily:'"SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace',fontSize:13,cursorBlink:!0,scrollback:8e3}),r=new se;a.loadAddon(r),a.open(p),w.current={term:a,fit:r},f&&a.focus();const y=()=>{a.options.theme=R()};window.addEventListener(I,y),a.attachCustomKeyEventHandler(d=>{var E;if(d.type!=="keydown")return!0;const v=(E=d.key)==null?void 0:E.toLowerCase();if((d.ctrlKey||d.metaKey)&&v==="v")return!1;if(d.ctrlKey&&!d.shiftKey&&!d.altKey&&v==="c"&&a.hasSelection()){const F=a.textarea;return F&&(F.value=a.getSelection(),F.select()),!1}return!0});const C=b(n,o,{write:d=>a.write(d),dispose:()=>a.dispose()}),T=()=>{try{r.fit(),m({type:"terminal_resize",terminalId:o,conversationId:n,cols:a.cols,rows:a.rows})}catch{}},h=requestAnimationFrame(()=>{try{r.fit()}catch{}m(x?{type:"run_command",terminalId:o,conversationId:n,command:x,cols:a.cols,rows:a.rows}:{type:"terminal_create",terminalId:o,conversationId:n,cwd:t,cols:a.cols,rows:a.rows})}),k=a.onData(d=>{m({type:"terminal_input",terminalId:o,conversationId:n,data:d})});let N=null;return typeof ResizeObserver<"u"&&(N=new ResizeObserver(()=>{p.offsetWidth>0&&p.offsetHeight>0&&T()}),N.observe(p)),()=>{cancelAnimationFrame(h),k.dispose(),window.removeEventListener(I,y),N==null||N.disconnect(),C(),a.dispose(),w.current=null}},[n,o,g,m,b]),c.useEffect(()=>{if(!f)return;const p=requestAnimationFrame(()=>{const a=w.current;if(a){try{a.fit.fit(),m({type:"terminal_resize",terminalId:o,conversationId:n,cols:a.term.cols,rows:a.term.rows})}catch{}a.term.focus()}});return()=>cancelAnimationFrame(p)},[f]),s.jsx("div",{ref:j,className:`term-xterm ${f?"":"hidden"}`})}const B={name:"",command:"",cwd:"${pwd}"};function re({chat:n,send:o,terminal:x}){const t=L(),[f,m]=c.useState(null),[b,j]=c.useState(!1),[w,g]=c.useState(!1),[p,a]=c.useState(null),[r,y]=c.useState(B),[C,T]=c.useState(null),h=c.useRef(null),[k,N]=c.useState(!0);c.useEffect(()=>{n.terminals.length===0?m(null):n.terminals.some(e=>e.id===f)||m(n.terminals[n.terminals.length-1].id)},[n.terminals,f]),c.useEffect(()=>{n.terminalActiveId&&(m(n.terminalActiveId),j(!1))},[n.terminalActiveId]),c.useEffect(()=>()=>{h.current&&clearTimeout(h.current)},[]);const d=n.terminals.filter(e=>!e.agentBash),v=n.terminals.filter(e=>e.agentBash),E=e=>{var u;if(!n.ready)return;const i=Z(),l=n.activeConversationId||((u=n.state)==null?void 0:u.conversationId)||"";x.create({...e,id:i,conversationId:l,cols:e.cols??80,rows:e.rows??24,running:!0,exitCode:null}),m(i),j(!1)},F=()=>{var e;return E({title:t("terminalTitle",{n:d.length+1}),cwd:((e=n.state)==null?void 0:e.cwd)??""})},$=e=>{var u;const i=e.name||e.command,l=n.terminals.find(_=>_.title===i);if(l){x.restart(l.id),m(l.id),o({type:"run_command",terminalId:l.id,conversationId:l.conversationId,command:e,cols:80,rows:24});return}E({title:i,cwd:((u=n.state)==null?void 0:u.cwd)??"",command:e})},O=e=>{const i=n.terminals.find(l=>l.id===e);if(i&&o({type:"terminal_kill",terminalId:e,conversationId:i.conversationId}),x.close(e),f===e){const l=n.terminals.filter(u=>u.id!==e);m(l.length>0?l[l.length-1].id:null)}},D=e=>s.jsxs("div",{className:`term-tab ${e.id===f?"active":""}`,children:[s.jsxs("button",{type:"button",className:"term-tab-main",title:`${e.cwd}${e.command?`
1
+ import{a as c,j as s}from"./markdown-DRBrS2Nf.js";import{b as R,T as I,u as L,F as J,a as K,c as X,d as Q,e as U,f as V,g as W,h as Y,r as Z}from"./index-BVuOleif.js";import{D as ee,o as se}from"./xterm-D1D2FVe3.js";import"./react-C9ovnpIm.js";function ne({conversationId:n,terminalId:o,command:x,cwd:t,active:f,send:m,register:b}){const j=c.useRef(null),w=c.useRef(null),g=x?JSON.stringify(x):"";return c.useEffect(()=>{const p=j.current;if(!p)return;const a=new ee({theme:R(),fontFamily:'"SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace',fontSize:13,cursorBlink:!0,scrollback:8e3}),r=new se;a.loadAddon(r),a.open(p),w.current={term:a,fit:r},f&&a.focus();const y=()=>{a.options.theme=R()};window.addEventListener(I,y),a.attachCustomKeyEventHandler(d=>{var E;if(d.type!=="keydown")return!0;const v=(E=d.key)==null?void 0:E.toLowerCase();if((d.ctrlKey||d.metaKey)&&v==="v")return!1;if(d.ctrlKey&&!d.shiftKey&&!d.altKey&&v==="c"&&a.hasSelection()){const F=a.textarea;return F&&(F.value=a.getSelection(),F.select()),!1}return!0});const C=b(n,o,{write:d=>a.write(d),dispose:()=>a.dispose()}),T=()=>{try{r.fit(),m({type:"terminal_resize",terminalId:o,conversationId:n,cols:a.cols,rows:a.rows})}catch{}},h=requestAnimationFrame(()=>{try{r.fit()}catch{}m(x?{type:"run_command",terminalId:o,conversationId:n,command:x,cols:a.cols,rows:a.rows}:{type:"terminal_create",terminalId:o,conversationId:n,cwd:t,cols:a.cols,rows:a.rows})}),k=a.onData(d=>{m({type:"terminal_input",terminalId:o,conversationId:n,data:d})});let N=null;return typeof ResizeObserver<"u"&&(N=new ResizeObserver(()=>{p.offsetWidth>0&&p.offsetHeight>0&&T()}),N.observe(p)),()=>{cancelAnimationFrame(h),k.dispose(),window.removeEventListener(I,y),N==null||N.disconnect(),C(),a.dispose(),w.current=null}},[n,o,g,m,b]),c.useEffect(()=>{if(!f)return;const p=requestAnimationFrame(()=>{const a=w.current;if(a){try{a.fit.fit(),m({type:"terminal_resize",terminalId:o,conversationId:n,cols:a.term.cols,rows:a.term.rows})}catch{}a.term.focus()}});return()=>cancelAnimationFrame(p)},[f]),s.jsx("div",{ref:j,className:`term-xterm ${f?"":"hidden"}`})}const B={name:"",command:"",cwd:"${pwd}"};function re({chat:n,send:o,terminal:x}){const t=L(),[f,m]=c.useState(null),[b,j]=c.useState(!1),[w,g]=c.useState(!1),[p,a]=c.useState(null),[r,y]=c.useState(B),[C,T]=c.useState(null),h=c.useRef(null),[k,N]=c.useState(!0);c.useEffect(()=>{n.terminals.length===0?m(null):n.terminals.some(e=>e.id===f)||m(n.terminals[n.terminals.length-1].id)},[n.terminals,f]),c.useEffect(()=>{n.terminalActiveId&&(m(n.terminalActiveId),j(!1))},[n.terminalActiveId]),c.useEffect(()=>()=>{h.current&&clearTimeout(h.current)},[]);const d=n.terminals.filter(e=>!e.agentBash),v=n.terminals.filter(e=>e.agentBash),E=e=>{var u;if(!n.ready)return;const i=Z(),l=n.activeConversationId||((u=n.state)==null?void 0:u.conversationId)||"";x.create({...e,id:i,conversationId:l,cols:e.cols??80,rows:e.rows??24,running:!0,exitCode:null}),m(i),j(!1)},F=()=>{var e;return E({title:t("terminalTitle",{n:d.length+1}),cwd:((e=n.state)==null?void 0:e.cwd)??""})},$=e=>{var u;const i=e.name||e.command,l=n.terminals.find(_=>_.title===i);if(l){x.restart(l.id),m(l.id),o({type:"run_command",terminalId:l.id,conversationId:l.conversationId,command:e,cols:80,rows:24});return}E({title:i,cwd:((u=n.state)==null?void 0:u.cwd)??"",command:e})},O=e=>{const i=n.terminals.find(l=>l.id===e);if(i&&o({type:"terminal_kill",terminalId:e,conversationId:i.conversationId}),x.close(e),f===e){const l=n.terminals.filter(u=>u.id!==e);m(l.length>0?l[l.length-1].id:null)}},D=e=>s.jsxs("div",{className:`term-tab ${e.id===f?"active":""}`,children:[s.jsxs("button",{type:"button",className:"term-tab-main",title:`${e.cwd}${e.command?`
2
2
  > ${e.command.command}`:""}`,onClick:()=>{m(e.id),j(!1)},children:[s.jsx("span",{className:`term-tab-dot ${e.running?"run":"exit"}`}),s.jsxs("span",{className:"term-tab-title",children:[e.title,!e.running&&s.jsx("span",{className:"term-tab-exit",children:t("exited",{code:e.exitCode===null?"":` ${e.exitCode}`})})]})]}),s.jsx("button",{type:"button",className:"term-tab-close",title:t("closeTerminal"),onClick:()=>O(e.id),children:s.jsx(Y,{})})]},e.id),M=()=>{g(!0),a(null),y(B)},z=e=>{const i=n.commands[e];i&&(g(!1),a(e),y({name:i.name,command:i.command,cwd:i.cwd??""}))},A=()=>{g(!1),a(null)},S=()=>{const e=r.name.trim(),i=r.command.trim();if(!e||!i)return;const l=r.cwd.trim(),u={name:e,command:i,cwd:l||void 0},_=w?[...n.commands,u]:p!==null?n.commands.map((q,G)=>G===p?u:q):n.commands;o({type:"save_commands",commands:_}),A()},H=e=>{if(C===e){const i=n.commands.filter((l,u)=>u!==e);o({type:"save_commands",commands:i}),T(null),h.current&&clearTimeout(h.current)}else T(e),h.current&&clearTimeout(h.current),h.current=setTimeout(()=>T(null),2500)},P=w||p!==null;return s.jsxs("div",{className:"terminal-view",children:[s.jsxs("aside",{className:`term-side term-commands ${b?"open":""}`,children:[s.jsxs("div",{className:"panel-header",children:[s.jsx("span",{className:"panel-title",children:t("commands")}),s.jsxs("div",{className:"panel-header-actions",children:[s.jsx("button",{type:"button",className:"panel-refresh",title:t("rerun"),onClick:()=>o({type:"list_commands"}),children:s.jsx(J,{})}),s.jsx("button",{type:"button",className:"panel-new",title:t("newCommand"),onClick:M,children:s.jsx(K,{})})]})]}),s.jsx("div",{className:"panel-body",children:P?s.jsxs("div",{className:"cmd-form",children:[s.jsx("label",{htmlFor:"cmd-name",children:t("name")}),s.jsx("input",{id:"cmd-name",className:"cmd-input",value:r.name,placeholder:t("exampleName"),autoFocus:!0,onChange:e=>y({...r,name:e.target.value})}),s.jsx("label",{htmlFor:"cmd-command",children:t("command")}),s.jsx("input",{id:"cmd-command",className:"cmd-input",value:r.command,placeholder:t("exampleCommand"),onChange:e=>y({...r,command:e.target.value}),onKeyDown:e=>{e.key==="Enter"&&!e.nativeEvent.isComposing&&S()}}),s.jsxs("label",{htmlFor:"cmd-cwd",children:[t("directory")," ",s.jsx("span",{className:"cmd-hint",children:t("cwdHint")})]}),s.jsx("input",{id:"cmd-cwd",className:"cmd-input",value:r.cwd,placeholder:"${pwd}",onChange:e=>y({...r,cwd:e.target.value}),onKeyDown:e=>{e.key==="Enter"&&!e.nativeEvent.isComposing&&S()}}),s.jsxs("div",{className:"cmd-form-actions",children:[s.jsx("button",{type:"button",className:"btn",onClick:A,children:t("cancel")}),s.jsx("button",{type:"button",className:"btn primary",disabled:!r.name.trim()||!r.command.trim(),onClick:S,children:t("save")})]})]}):s.jsxs(s.Fragment,{children:[n.commands.length===0&&s.jsx("div",{className:"panel-empty",children:t("noCommands")}),n.commands.map((e,i)=>s.jsxs("div",{className:"cmd-item",children:[s.jsx("button",{type:"button",className:"cmd-run",title:t("clickToRun"),onClick:()=>$(e),children:s.jsx(X,{})}),s.jsxs("button",{type:"button",className:"cmd-main",title:t("clickToRun"),onClick:()=>$(e),children:[s.jsx("span",{className:"cmd-name",children:e.name}),s.jsx("span",{className:"cmd-command",children:e.command}),e.cwd&&s.jsx("span",{className:"cmd-cwd",children:e.cwd})]}),s.jsx("button",{type:"button",className:"cmd-act",title:t("edit"),onClick:()=>z(i),children:s.jsx(Q,{})}),s.jsx("button",{type:"button",className:`cmd-act del ${C===i?"confirm":""}`,title:t("delete"),onClick:()=>H(i),children:C===i?t("confirmQ"):s.jsx(U,{})})]},i))]})}),s.jsxs("div",{className:"term-tabs-block",children:[s.jsxs("div",{className:"panel-header",children:[s.jsx("span",{className:"panel-title",children:t("terminal")}),s.jsx("button",{type:"button",className:"panel-new",title:t("newTerminal"),onClick:F,children:s.jsx(K,{})})]}),s.jsxs("div",{className:"panel-body",children:[n.terminals.length===0&&s.jsx("div",{className:"panel-empty",children:t("noTerminal")}),d.map(D),v.length>0&&s.jsxs("div",{className:"term-folder",children:[s.jsxs("button",{type:"button",className:`term-folder-header ${k?"open":""}`,title:t("aiBashGroup"),onClick:()=>N(e=>!e),children:[s.jsx("span",{className:"term-folder-caret",children:k?"▾":"▸"}),s.jsx("span",{className:"term-folder-title",children:t("aiBashGroup")}),s.jsx("span",{className:"term-folder-count",children:v.length})]}),k&&s.jsx("div",{className:"term-folder-body",children:v.map(D)})]})]})]})]}),s.jsxs("div",{className:"term-main",children:[b&&s.jsx("div",{className:"drawer-backdrop",onClick:()=>j(!1)}),s.jsx("button",{type:"button",className:"term-side-toggle",title:t("commands"),onClick:()=>j(e=>!e),children:s.jsx(V,{})}),n.terminals.length===0?s.jsxs("div",{className:"term-empty",children:[s.jsx(W,{className:"term-empty-icon"}),s.jsx("div",{className:"term-empty-title",children:t("builtinTerminal")}),s.jsx("div",{className:"term-empty-sub",children:t("termEmptySub")})]}):n.terminals.map(e=>s.jsx(ne,{conversationId:e.conversationId,terminalId:e.id,command:e.command,cwd:e.cwd,active:e.id===f,send:o,register:x.register},`${e.conversationId}:${e.id}`))]})]})}export{re as TerminalPanel};