pi-web-ui 0.93.0 → 0.94.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.
package/CHANGELOG.md CHANGED
@@ -10,14 +10,29 @@
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.94.0] — 2026-09-22
14
+
15
+ ### Added
16
+
17
+ - **复制为图片:预览面板 + 标题 / 边框 / 水印 + 多轮勾选拼接(#274)** — 点「复制为图片」打开右侧停靠面板(不挡对话)。可选标题、边框、自定义水印(默认 `pi-web-ui`);对话里勾选多条消息,按时间线从早到晚竖排拼成一张 2x PNG。面板提供「包含工具调用 / 包含思考过程」两个开关(默认关),勾上后把对应块加进图并强制展开,不改对话里原有折叠状态。超长则降到 1x 或拒绝复制,避免黑图。
18
+ - **压缩软上限(Soft Cap)支持人性化 tokens 单位输入与纯数字智能识别** — 设置「消息显示」页与按模型覆盖的压缩阈值输入框全面支持人类习惯的缩写(如 `300k`、`1.5M`、`300,000`、`300_000`);纯数字且 `<= 1000`(如 `300`、`128`、`64`)自动智能识别为 K tokens(`300` → `300,000`),回显自动格式化为整千/整百万可读缩写,避免手滑漏输 0。
19
+ - **全局跨标签页/重启共享的项目模型与 Provider Key 记忆** — 将项目绑定的模型与服务商密钥提升至全局持久化层(`GLOBAL_SETTINGS_KEY`)。新开标签页、切换工作区或重启浏览器时,确定恢复该项目最后使用的模型与密钥;全新空白会话创建时提前解析并注入目标模型,且在 `setModel` 前优先恢复对应的 provider key,彻底解决新对话鉴权失败与回退内置硬编码模型的问题。
20
+
21
+ ### Changed
22
+
23
+ - **官方插件清单内置为默认来源(`PI_WEB_PLUGIN_CATALOG_URL`)** —— 服务端启动时未配置该环境变量时,自动拉取官方社区清单 `https://xing-shuyin.github.io/pi-web-ui-plugins/catalog.json` 并同步进插件市场列表(**仅更新列表供用户按需安装,不自动安装插件**);显式设为空串或 `off`/`0`/`false`/`no` 可关闭;仅当显式设置 `PI_WEB_PLUGIN_CATALOG_INSTALL=1` 时才在开机时顺手自动安装全部插件(headless/容器预置镜像场景)。
24
+ - **界面交互防选区干扰与遮罩层重绘优化** —— 侧边栏(会话列表、项目列表、文件树)、顶栏、右键菜单、消息头部及技能卡片头部等不可交互文本区域增加 `user-select: none`,防止高频双击或拖拽时意外选中文本;弹窗与文件预览遮罩层移除 `backdrop-filter: blur` 改用纯色半透明实底,并添加 `overscroll-behavior: contain` 与硬件加速,消除滚动穿透并显著降低大消息流时的重绘负担。
25
+
26
+ ### Fixed
27
+
28
+ - **复制为图片浅色主题色差(#273)** — html-to-image 把 `color-mix(...)` / 半透明 `rgba` 画到默认黑画布上,浅色气泡变成深紫、深字叠黑底。导出前把计算色拍成不透明 rgb,画布底用主题 `--card-bg`/`--bg` 实底,并去掉 `backdrop-filter`(否则 SVG 里会变成黑罩)。
29
+
13
30
  <!-- auto-i18n:start -->
14
31
  ### i18n
15
32
 
16
- - 前端新增 key(34):`themeLight`、`themeDark`、`quickPhrasesSendTip`、`persistSubagent`、`toolImages`、`toolImagesDesc`、`toolImageZoom`、`toolWatchdogTimeout`、`toolWatchdogTimeoutDesc`、`toolWatchdogOff`、`uiLayoutContextToolcall`、`pluginSettingsTitle`、`pluginSettingsShow`、`pluginSettingsHide`、`claimFilesEnabledDesc`、`claimFilesOffHint`、`toolInfoMenuLabel`、`toolInfoTitle`、`toolInfoLoading`、`toolInfoUnsupported`、`toolInfoMissing`、`toolInfoActive`、`toolInfoInactive`、`toolInfoSource`、`toolInfoDescription`、`toolInfoNoDescription`、`toolInfoPromptSnippet`、`toolInfoGuidelines`、`toolInfoParams`、`toolInfoParamsNone`、`toolInfoSchemaDropped`、`toolInfoRawSchema`、`toolInfoRequired`、`toolInfoFootnote`
17
- - 前端中文变更(1):`elsewhereTip`
18
- - 前端英文变更(1):`elsewhereTip`
19
- - 服务端新增 key(1):`agent.conv.limit.reached`
20
- - 服务端文案变更(2):`subagents.spawn.started`、`subagents.list.empty`
33
+ - 前端新增 key(13):`saveAsImage`、`copyImageBtn`、`savingImage`、`imageTitle`、`imageTitlePlaceholder`、`imageBorder`、`imageWatermark`、`imageWatermarkPlaceholder`、`exportSelectHint`、`exportTooLong`、`exportSelectedCount`、`exportIncludeTools`、`exportIncludeThinking`
34
+ - 前端中文变更(2):`softCapHint`、`softCapOff`
35
+ - 前端英文变更(2):`softCapHint`、`softCapOff`
21
36
  <!-- auto-i18n:end -->
22
37
 
23
38
  ## [0.93.0] — 2026-09-21
@@ -1179,6 +1194,7 @@ when?, children?}`,也收 `topbar` / `settings` 这类简写别名);宿主
1179
1194
  - 0.29.0(2026-08-23):全局搜索弹窗(Ctrl+K)+ 消息列表惰性窗口化。
1180
1195
 
1181
1196
  [Unreleased]: https://github.com/xing-shuyin/pi-web-ui/compare/v0.93.0...main
1197
+ [0.94.0]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.94.0
1182
1198
  [0.93.0]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.93.0
1183
1199
  [0.92.0]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.92.0
1184
1200
  [0.91.0]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.91.0
package/README.md CHANGED
@@ -712,7 +712,7 @@ All optional — the defaults are what the app is developed against. Full refere
712
712
  | `PI_WEB_SDK` | `bundled` | Which pi SDK copy to run: `bundled` (the copy shipped with pi-web-ui) or `global` (the nearest ancestor copy when it is newer). |
713
713
  | `PI_WEB_ALLOW_ORIGINS` | empty | Extra comma-separated `Origin` allow-list for the WebSocket check (dev proxy / reverse proxy). |
714
714
  | `PI_WEB_GIT_EXTENSION_CHECK` | `1` (on) | `0` / `false` / `no` / `off` drops the git-source row from “Check all updates” (it runs `git ls-remote`), for very large repos. |
715
- | `PI_WEB_PLUGIN_CATALOG_URL` | empty | Boot-time catalog pre-sync: fetch a catalog document (http(s) URL or absolute path), write the installable list, then install/update every entry (failures only warn). |
715
+ | `PI_WEB_PLUGIN_CATALOG_URL` | official catalog URL | Boot-time catalog source: defaults to the official community registry `https://xing-shuyin.github.io/pi-web-ui-plugins/catalog.json` (writes installable list only, **does not install plugins**). Set to empty or `off`/`0`/`false`/`no` to disable; combine with `PI_WEB_PLUGIN_CATALOG_INSTALL=1` to auto-install entries at boot (headless/container provisioning). |
716
716
  | `PI_WEB_LAUNCHED_BY` / `PI_WEB_SERVICE_NAME` | empty | Written by `pi-web-ui server install` into the service unit/launcher (`service` / the `--name`): tells the server a supervisor owns it, which is what enables the update panel's “Restart service” button. |
717
717
  | `PI_WEB_DSH_*` | — | DSH runtime knobs: `PI_WEB_DSH_RUNTIME`, `PI_WEB_DSH_DATA_DIR`, `PI_WEB_DSH_PATCH_DIR`, `PI_WEB_DSH_QUESTION_TIMEOUT_MS`, `PI_WEB_DSH_TOOL_TIMEOUT_MS`, `PI_WEB_DSH_SESSION_RETENTION_DAYS`, `PI_WEB_DSH_DEBUG`. |
718
718
 
package/README.zh-CN.md CHANGED
@@ -595,7 +595,7 @@ pi-web-ui uninstall <id> # 卸载插件
595
595
  | `PI_WEB_SDK` | `bundled` | 用哪一份 pi SDK:`bundled`(pi-web-ui 自带的副本)或 `global`(祖先链上更新的那份)。 |
596
596
  | `PI_WEB_ALLOW_ORIGINS` | 空 | WebSocket Origin 校验的额外白名单(逗号分隔;dev 代理 / 反向代理用)。 |
597
597
  | `PI_WEB_GIT_EXTENSION_CHECK` | `1`(默认开) | 设 `0`/`false`/`no`/`off` 关闭「全部组件更新」里的 git 行(走 `git ls-remote`),大型单体仓库场景可用。 |
598
- | `PI_WEB_PLUGIN_CATALOG_URL` | 空 | 开机插件目录预同步:拉取目录文档(http(s) URL 或本地绝对路径)→ 写可安装列表 → 逐条安装/更新(失败只告警)。 |
598
+ | `PI_WEB_PLUGIN_CATALOG_URL` | 官方清单 URL | 开机插件市场清单来源:默认指向官方社区清单 `https://xing-shuyin.github.io/pi-web-ui-plugins/catalog.json`(仅拉取文档写可安装列表,**不自动安装插件**)。设为空串或 `off`/`0`/`false`/`no` 可关闭;设 `PI_WEB_PLUGIN_CATALOG_INSTALL=1` 时顺手自动全部安装。 |
599
599
  | `PI_WEB_LAUNCHED_BY` / `PI_WEB_SERVICE_NAME` | 空 | 由 `pi-web-ui server install` 写进服务单元/启动脚本(`service` / `--name`):服务端据此知道实例由平台服务托管,更新面板才会出现「重启服务」按钮。 |
600
600
  | `PI_WEB_DSH_*` | — | DSH 运行时旋钮:`PI_WEB_DSH_RUNTIME`、`PI_WEB_DSH_DATA_DIR`、`PI_WEB_DSH_PATCH_DIR`、`PI_WEB_DSH_QUESTION_TIMEOUT_MS`、`PI_WEB_DSH_TOOL_TIMEOUT_MS`、`PI_WEB_DSH_SESSION_RETENTION_DAYS`、`PI_WEB_DSH_DEBUG`。 |
601
601
 
@@ -1924,6 +1924,23 @@ export class ClientSession {
1924
1924
  });
1925
1925
  }
1926
1926
  await cs.bindSession();
1927
+ // 同步全局默认模型至 SDK settingsManager(若 settings.json 尚未写入),防底层 session 创建时 findInitialModel 兜底回退硬编码模型
1928
+ const globalDefault = stateStore.getDefaultModel();
1929
+ if (globalDefault) {
1930
+ const slash = globalDefault.indexOf("/");
1931
+ if (slash > 0 && slash < globalDefault.length - 1) {
1932
+ const p = globalDefault.slice(0, slash);
1933
+ const id = globalDefault.slice(slash + 1);
1934
+ try {
1935
+ if (!cs.session.settingsManager.getDefaultModel()) {
1936
+ cs.session.settingsManager.setDefaultModelAndProvider(p, id);
1937
+ }
1938
+ }
1939
+ catch {
1940
+ /* 会话未就绪时忽略 */
1941
+ }
1942
+ }
1943
+ }
1927
1944
  await cs.restoreProjectProviderKeysForCwd(cwd);
1928
1945
  await cs.restoreProjectModelForCwd(cwd);
1929
1946
  return cs;
@@ -1937,7 +1954,7 @@ export class ClientSession {
1937
1954
  * 应用(prompt replace/append + 白名单),其余(终端接管、Windows persona
1938
1955
  * 等)仍跟随主会话设置。undefined = 按主会话设置(普通对话/不选模板的子代理)。
1939
1956
  */
1940
- makeRuntimeFactory(terminals, apply, ownerId) {
1957
+ makeRuntimeFactory(terminals, apply, ownerId, initialModel) {
1941
1958
  return async ({ cwd: effectiveCwd, sessionManager }) => {
1942
1959
  const services = await createAgentSessionServices({
1943
1960
  cwd: effectiveCwd,
@@ -2077,9 +2094,38 @@ export class ClientSession {
2077
2094
  // anchor 在拿到 created.session 后回填(SDK 的 runtime.session 就是它)。
2078
2095
  const bridgeAnchor = {};
2079
2096
  const bridge = this.bridgeTarget(bridgeAnchor, ownerId);
2097
+ // 为全新会话(0 条消息的空白对话/新对话)提前解析目标模型并注入,
2098
+ // 避免 SDK findInitialModel 在无 model 时回退到内置硬编码默认(如 deepseek-v4-pro):
2099
+ let sessionModel = initialModel;
2100
+ if (!sessionModel) {
2101
+ const isBlank = sessionManager.buildSessionContext().messages.length === 0;
2102
+ if (isBlank) {
2103
+ const savedModelId = this.stateStore.getProjectModel(this.clientId, effectiveCwd) ?? this.stateStore.getDefaultModel();
2104
+ if (savedModelId) {
2105
+ const slash = savedModelId.indexOf("/");
2106
+ if (slash > 0 && slash < savedModelId.length - 1) {
2107
+ const p = savedModelId.slice(0, slash);
2108
+ const id = savedModelId.slice(slash + 1);
2109
+ const found = services.modelRuntime.getModel(p, id);
2110
+ if (found) {
2111
+ try {
2112
+ await this.restoreKeyForModel(savedModelId, effectiveCwd);
2113
+ if (services.modelRuntime.hasConfiguredAuth(p)) {
2114
+ sessionModel = found;
2115
+ }
2116
+ }
2117
+ catch {
2118
+ /* 密钥恢复失败则由 SDK 自行解析 */
2119
+ }
2120
+ }
2121
+ }
2122
+ }
2123
+ }
2124
+ }
2080
2125
  const created = await createAgentSessionFromServices({
2081
2126
  services,
2082
2127
  sessionManager,
2128
+ model: sessionModel,
2083
2129
  // 覆盖 SDK 内置 bash(customTools 按 name 覆盖)。双实现分流:
2084
2130
  // 「默认 bash 覆盖」开关(terminalBash)关 → 原生 SDK bash(纯进程、不开终端);
2085
2131
  // 开 → 终端接管 bash(persist 决定一次性/持久,可静默自动转后台)。
@@ -5581,7 +5627,7 @@ export class ClientSession {
5581
5627
  try {
5582
5628
  const conversationId = this.nextConversationId();
5583
5629
  const terminals = this.makeTerminalManager(conversationId, this.cwd);
5584
- const runtime = await createAgentSessionRuntime(this.makeRuntimeFactory(terminals, undefined, conversationId), {
5630
+ const runtime = await createAgentSessionRuntime(this.makeRuntimeFactory(terminals, undefined, conversationId, prevModel ?? undefined), {
5585
5631
  cwd: this.cwd,
5586
5632
  agentDir: this.agentDir,
5587
5633
  sessionManager: SessionManager.create(this.cwd),
@@ -5597,15 +5643,27 @@ export class ClientSession {
5597
5643
  this.invalidateSessionInfos();
5598
5644
  // New session seeds with the ModelRuntime default model — restore the
5599
5645
  // model the user had selected in the previous chat.
5600
- if (prevModel && this.sharedModelRuntime) {
5646
+ let modelRestored = !!this.session.model;
5647
+ if (!modelRestored && prevModel && this.sharedModelRuntime) {
5601
5648
  try {
5602
- await this.session.setModel(prevModel);
5603
5649
  const p = prevModel.provider;
5604
5650
  const mid = `${p}/${prevModel.id}`;
5651
+ // 先恢复 provider key,再 setModel(否则 checkAuth 鉴权失败)
5605
5652
  await this.restoreKeyForModel(mid, this.cwd);
5653
+ await this.session.setModel(prevModel);
5654
+ modelRestored = true;
5606
5655
  }
5607
5656
  catch {
5608
- // model no longer resolvable — keep the default
5657
+ // model no longer resolvable
5658
+ }
5659
+ }
5660
+ if (!modelRestored) {
5661
+ // 上个会话模型未能恢复(或无上个会话):回落项目记忆或全局默认模型
5662
+ try {
5663
+ await this.restoreProjectModelForCwd(this.cwd);
5664
+ }
5665
+ catch {
5666
+ /* 保持默认 */
5609
5667
  }
5610
5668
  }
5611
5669
  if (prevThinking) {
@@ -7207,9 +7265,10 @@ export class ClientSession {
7207
7265
  // Restore the previously-selected model on the forked branch.
7208
7266
  if (prevModel && this.sharedModelRuntime) {
7209
7267
  try {
7210
- await this.session.setModel(prevModel);
7211
7268
  const pm = prevModel;
7269
+ // 先恢复 provider key,再 setModel(否则 checkAuth 鉴权失败)
7212
7270
  await this.restoreKeyForModel(`${pm.provider}/${pm.id}`, this.cwd);
7271
+ await this.session.setModel(prevModel);
7213
7272
  }
7214
7273
  catch {
7215
7274
  // model no longer resolvable — keep the default
@@ -7808,8 +7867,9 @@ export class ClientSession {
7808
7867
  const model = mr.getModel(provider, id);
7809
7868
  if (!model)
7810
7869
  throw new Error(`模型不存在:${modelId}`);
7811
- await this.session.setModel(model);
7870
+ // 先恢复 provider key,再 setModel(否则 checkAuth 鉴权失败)
7812
7871
  await this.restoreKeyForModel(modelId, this.cwd);
7872
+ await this.session.setModel(model);
7813
7873
  // Immediately remember the model + the key it uses for the current
7814
7874
  // project (not only after a turn). This is what makes project switching
7815
7875
  // restore both the model and the provider key.
@@ -7846,6 +7906,13 @@ export class ClientSession {
7846
7906
  const active = this.modelAdmin.getActiveKeyName(provider);
7847
7907
  if (active)
7848
7908
  this.stateStore.saveDefaultProviderKey(provider, active);
7909
+ // 同步写入 SDK 的 settingsManager,使底层 session 创建时 findInitialModel 也能识别该默认模型
7910
+ try {
7911
+ this.session.settingsManager.setDefaultModelAndProvider(provider, id);
7912
+ }
7913
+ catch {
7914
+ /* 会话未就绪时忽略 */
7915
+ }
7849
7916
  this.pushDefaultModel();
7850
7917
  this.emit({
7851
7918
  type: "notice",
@@ -7867,6 +7934,12 @@ export class ClientSession {
7867
7934
  /** Clear the GLOBAL default model (new projects fall back to the SDK default). */
7868
7935
  clearDefaultModel() {
7869
7936
  this.stateStore.clearDefaultModel();
7937
+ try {
7938
+ this.session.settingsManager.setDefaultModelAndProvider(undefined, undefined);
7939
+ }
7940
+ catch {
7941
+ /* 忽略 */
7942
+ }
7870
7943
  this.pushDefaultModel();
7871
7944
  this.emit({
7872
7945
  type: "notice",
@@ -591,11 +591,28 @@ export class ClientStateStore {
591
591
  }
592
592
  /** Get a single provider's saved key for a project. */
593
593
  getProjectProviderKey(clientId, cwd, provider) {
594
- return this.load()[clientId]?.projectProviderKeys?.[cwd]?.[provider];
594
+ const all = this.load();
595
+ const globalHit = all[ClientStateStore.GLOBAL_SETTINGS_KEY]?.projectProviderKeys?.[cwd]?.[provider];
596
+ if (globalHit)
597
+ return globalHit;
598
+ const direct = all[clientId]?.projectProviderKeys?.[cwd]?.[provider];
599
+ if (direct)
600
+ return direct;
601
+ for (const [id, state] of Object.entries(all)) {
602
+ if (id === clientId || id === ClientStateStore.GLOBAL_SETTINGS_KEY)
603
+ continue;
604
+ const hit = state.projectProviderKeys?.[cwd]?.[provider];
605
+ if (hit)
606
+ return hit;
607
+ }
608
+ return undefined;
595
609
  }
596
610
  /** Remember which key was last used for a provider in a project. */
597
611
  saveProjectProviderKey(clientId, cwd, provider, keyName) {
598
612
  const all = this.load();
613
+ const globalState = (all[ClientStateStore.GLOBAL_SETTINGS_KEY] ??= { projects: [] });
614
+ const gMap = (globalState.projectProviderKeys ??= {});
615
+ (gMap[cwd] ??= {})[provider] = keyName;
599
616
  const state = (all[clientId] ??= { projects: [] });
600
617
  const map = (state.projectProviderKeys ??= {});
601
618
  const inner = (map[cwd] ??= {});
@@ -605,6 +622,13 @@ export class ClientStateStore {
605
622
  /** Delete a per-project provider key (e.g. when the key is removed). */
606
623
  deleteProjectProviderKey(clientId, cwd, provider) {
607
624
  const all = this.load();
625
+ const gInner = all[ClientStateStore.GLOBAL_SETTINGS_KEY]?.projectProviderKeys?.[cwd];
626
+ if (gInner && provider in gInner) {
627
+ delete gInner[provider];
628
+ if (Object.keys(gInner).length === 0) {
629
+ delete all[ClientStateStore.GLOBAL_SETTINGS_KEY].projectProviderKeys[cwd];
630
+ }
631
+ }
608
632
  const inner = all[clientId]?.projectProviderKeys?.[cwd];
609
633
  if (!inner || !(provider in inner))
610
634
  return;
@@ -672,11 +696,39 @@ export class ClientStateStore {
672
696
  }
673
697
  /** Get the model the user last selected in a project, or undefined. */
674
698
  getProjectModel(clientId, cwd) {
675
- return this.load()[clientId]?.projectModels?.[cwd];
699
+ const all = this.load();
700
+ // 1. 全局项目模型记忆(跨标签页、跨客户端、重启浏览器共享,优先级最高)
701
+ const globalHit = all[ClientStateStore.GLOBAL_SETTINGS_KEY]?.projectModels?.[cwd];
702
+ if (globalHit)
703
+ return globalHit;
704
+ // 2. 本客户端记忆
705
+ const direct = all[clientId]?.projectModels?.[cwd];
706
+ if (direct)
707
+ return direct;
708
+ // 3. 其他客户端按最近活跃时间倒序查找兜底
709
+ const candidates = [];
710
+ for (const [id, state] of Object.entries(all)) {
711
+ if (id === clientId || id === ClientStateStore.GLOBAL_SETTINGS_KEY)
712
+ continue;
713
+ const m = state.projectModels?.[cwd];
714
+ if (m) {
715
+ const p = state.projects?.find((proj) => proj.path === cwd);
716
+ candidates.push({ model: m, lastUsed: p?.lastUsed ?? 0 });
717
+ }
718
+ }
719
+ if (candidates.length > 0) {
720
+ candidates.sort((a, b) => b.lastUsed - a.lastUsed);
721
+ return candidates[0].model;
722
+ }
723
+ return undefined;
676
724
  }
677
725
  /** Remember the model last selected in a project (immediate, not after a turn). */
678
726
  saveProjectModel(clientId, cwd, modelId) {
679
727
  const all = this.load();
728
+ // 写入全局项目记忆(保证新标签页/重启浏览器时确定可用)
729
+ const globalState = (all[ClientStateStore.GLOBAL_SETTINGS_KEY] ??= { projects: [] });
730
+ (globalState.projectModels ??= {})[cwd] = modelId;
731
+ // 同时写入本客户端
680
732
  const state = (all[clientId] ??= { projects: [] });
681
733
  (state.projectModels ??= {})[cwd] = modelId;
682
734
  this.save();
@@ -685,6 +737,12 @@ export class ClientStateStore {
685
737
  * removed from the catalog). */
686
738
  deleteProjectModel(clientId, cwd) {
687
739
  const all = this.load();
740
+ const gMap = all[ClientStateStore.GLOBAL_SETTINGS_KEY]?.projectModels;
741
+ if (gMap && cwd in gMap) {
742
+ delete gMap[cwd];
743
+ if (Object.keys(gMap).length === 0)
744
+ delete all[ClientStateStore.GLOBAL_SETTINGS_KEY].projectModels;
745
+ }
688
746
  const map = all[clientId]?.projectModels;
689
747
  if (!map || !(cwd in map))
690
748
  return;
@@ -78,7 +78,7 @@ export function absoluteParent(wire) {
78
78
  return null;
79
79
  const i = s.lastIndexOf("/");
80
80
  if (i < 0)
81
- return IS_WIN32 && /^[A-Za-z]:$/.test(s) ? MACHINE_ROOT : null;
81
+ return /^[A-Za-z]:$/.test(s) ? MACHINE_ROOT : null;
82
82
  if (i === 0)
83
83
  return "/"; // posix "/a" → "/"
84
84
  return s.slice(0, i);
@@ -2382,24 +2382,36 @@ httpServer.listen(PORT, HOST, () => {
2382
2382
  });
2383
2383
  // 上传文件保留期清理:启动扫一次 + 每 6 小时一次(best-effort,见 uploads.ts)
2384
2384
  scheduleUploadCleanup();
2385
- // 开机目录预同步(issue #165):PI_WEB_PLUGIN_CATALOG_URL 指向一份插件市场目录文档
2386
- // (headless/预置场景,不开浏览器也能装插件)。只跑一次、失败只告警不阻断启动;
2387
- // 条目逐条安装/更新(托管实例上安装会被安装器拒绝,但列表本身仍会更新)。
2388
- const BOOT_CATALOG_URL = (process.env.PI_WEB_PLUGIN_CATALOG_URL ?? "").trim();
2389
- if (BOOT_CATALOG_URL) {
2390
- void syncPluginCatalog(BOOT_CATALOG_URL, { install: true }, {
2385
+ // 开机目录预同步(issue #165):拉取一份插件市场目录文档 → 写可安装列表 → 逐条安装/更新。
2386
+ // 官方社区清单(xing-shuyin/pi-web-ui-plugins,PR 经 CI 自动审核 + 构建发布)作为**默认来源**,
2387
+ // 开箱即用、无需配置 PI_WEB_PLUGIN_CATALOG_URL;显式设为空串或 off/0/false/no 可关闭。
2388
+ // 默认**仅同步市场目录列表,绝不自动安装插件**(用户在界面按需点击安装)。
2389
+ // 仅在显式配置 PI_WEB_PLUGIN_CATALOG_INSTALL=1/true 时才顺手全部安装(headless/容器预置镜像场景)。
2390
+ const OFFICIAL_PLUGIN_CATALOG_URL = "https://xing-shuyin.github.io/pi-web-ui-plugins/catalog.json";
2391
+ const BOOT_CATALOG_URL = process.env.PI_WEB_PLUGIN_CATALOG_URL === undefined
2392
+ ? OFFICIAL_PLUGIN_CATALOG_URL
2393
+ : process.env.PI_WEB_PLUGIN_CATALOG_URL.trim();
2394
+ const bootCatalogDisabled = !BOOT_CATALOG_URL || /^(0|off|false|no)$/i.test(BOOT_CATALOG_URL);
2395
+ const autoInstall = /^(1|true|yes)$/i.test(process.env.PI_WEB_PLUGIN_CATALOG_INSTALL ?? "");
2396
+ if (!bootCatalogDisabled) {
2397
+ void syncPluginCatalog(BOOT_CATALOG_URL, { install: autoInstall }, {
2391
2398
  customCatalogPath: pluginMgr.customCatalogPath,
2392
2399
  pluginsDir: join(DATA_DIR, "plugins"),
2393
2400
  installer: pluginInstaller,
2394
2401
  afterWrite: () => reloadPluginsAndPush(),
2395
2402
  }).then((r) => {
2396
2403
  if (!r.ok) {
2397
- console.warn(`[catalog] PI_WEB_PLUGIN_CATALOG_URL 同步失败(不阻断启动): ${r.error}`);
2404
+ console.warn(`[catalog] 插件目录预同步失败(不阻断启动): ${r.error}`);
2398
2405
  return;
2399
2406
  }
2400
- const bad = (r.installed ?? []).filter((i) => !i.ok);
2401
- console.log(`[catalog] PI_WEB_PLUGIN_CATALOG_URL 同步完成:安装 ${(r.installed ?? []).length - bad.length} 成功 / ${bad.length} 失败` +
2402
- (bad.length ? `:${bad.map((i) => `${i.id}(${i.error ?? "?"})`).join(";")}` : ""));
2407
+ if (autoInstall) {
2408
+ const bad = (r.installed ?? []).filter((i) => !i.ok);
2409
+ console.log(`[catalog] 插件目录预同步完成:安装 ${(r.installed ?? []).length - bad.length} 成功 / ${bad.length} 失败` +
2410
+ (bad.length ? `:${bad.map((i) => `${i.id}(${i.error ?? "?"})`).join(";")}` : ""));
2411
+ }
2412
+ else {
2413
+ console.log(`[catalog] 插件市场列表预同步完成(共 ${(r.entries ?? []).length} 个条目,按需安装)`);
2414
+ }
2403
2415
  });
2404
2416
  }
2405
2417
  // Local control socket (status / quiesce / unquiesce) — same data dir the
@@ -20,12 +20,46 @@ export const SOFT_CAP_MIN_TOKENS = 2000;
20
20
  export const SOFT_CAP_MIN_HEADROOM = 2048;
21
21
  /** 单客户端最多存多少条按模型覆盖(防手滑粘贴刷爆 client-state)。 */
22
22
  export const SOFT_CAP_MAX_OVERRIDES = 64;
23
- /** 归一化全局软上限:非数值/<=0 = 关闭;钳制到 [0, 10_000_000] 整数。 */
23
+ /** 归一化全局软上限:支持数字或带单位字符串(如 "300k", "1.5m", "300,000");
24
+ * <= 1000 的正数智能识别为 K tokens(如 300 -> 300,000);<=0 或非法 = 关闭;
25
+ * 钳制到 [0, 10_000_000] 整数。纯函数。 */
24
26
  export function normalizeSoftCapTokens(v) {
25
- const n = Math.floor(Number(v));
26
- if (!Number.isFinite(n) || n <= 0)
27
+ if (v === null || v === undefined)
27
28
  return 0;
28
- return Math.min(10_000_000, n);
29
+ if (typeof v === "number") {
30
+ if (!Number.isFinite(v) || v <= 0)
31
+ return 0;
32
+ const n = Math.floor(v);
33
+ if (n <= 1000)
34
+ return n * 1000;
35
+ return Math.min(10_000_000, n);
36
+ }
37
+ const s = String(v)
38
+ .trim()
39
+ .toLowerCase()
40
+ .replace(/[,_\s]/g, "");
41
+ if (!s)
42
+ return 0;
43
+ const m = s.match(/^(\d+(?:\.\d+)?)([km])?$/);
44
+ if (!m)
45
+ return 0;
46
+ const num = parseFloat(m[1]);
47
+ if (!Number.isFinite(num) || num <= 0)
48
+ return 0;
49
+ const unit = m[2];
50
+ let tokens;
51
+ if (unit === "k") {
52
+ tokens = Math.floor(num * 1_000);
53
+ }
54
+ else if (unit === "m") {
55
+ tokens = Math.floor(num * 1_000_000);
56
+ }
57
+ else {
58
+ tokens = Math.floor(num);
59
+ if (tokens <= 1000)
60
+ tokens *= 1000;
61
+ }
62
+ return Math.min(10_000_000, Math.max(0, tokens));
29
63
  }
30
64
  /** 归一化按模型覆盖:key = "provider/id" 非空(≤200 字),value 走
31
65
  * normalizeSoftCapTokens;0 值条目丢弃(= 用全局);上限 64 条。纯函数。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-ui",
3
- "version": "0.93.0",
3
+ "version": "0.94.0",
4
4
  "description": "Just open your browser to get all your work done — browser cockpit for AI coding agents (chat, code, files, terminal, Git in one tab)",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -1,3 +1,3 @@
1
- import{r as e}from"./rolldown-runtime-hePW80VL.js";import{i as t,t as n}from"./react-CR5VJ85Q.js";import{a as r,c as i,d as a,f as o,g as s,h as c,i as l,l as u,m as d,o as f,p,r as m,s as h,t as g,u as _}from"./index-B48QWf0I.js";import{n as v,t as y}from"./xterm-Cf5xxe4g.js";var b=e(t(),1);function x(e,t){return Math.abs(t)>10&&Math.abs(t)>Math.abs(e)*1.2}function S(e,t){return e-t}var C=n();function w(e){let t=null;return{clean:e.replace(/\r?\n?\[pi-term-exit:(-?\d+)\]\r?\n?/g,(e,n)=>(t=Number(n),`\r
1
+ import{r as e}from"./rolldown-runtime-hePW80VL.js";import{i as t,t as n}from"./react-CR5VJ85Q.js";import{a as r,c as i,d as a,f as o,g as s,h as c,i as l,l as u,m as d,o as f,p,r as m,s as h,t as g,u as _}from"./index-18jK6al2.js";import{n as v,t as y}from"./xterm-Cf5xxe4g.js";var b=e(t(),1);function x(e,t){return Math.abs(t)>10&&Math.abs(t)>Math.abs(e)*1.2}function S(e,t){return e-t}var C=n();function w(e){let t=null;return{clean:e.replace(/\r?\n?\[pi-term-exit:(-?\d+)\]\r?\n?/g,(e,n)=>(t=Number(n),`\r
2
2
  `)).replace(/\r?\n?\x1b\[90m\[(?:进程已退出,退出码 |Process exited with code )-?\d+\]\x1b\[0m\r?\n?/g,`\r
3
3
  `),exitCode:t}}function T({conversationId:e,terminalId:t,command:n,cwd:i,title:a,agentBash:o,active:s,running:c,exitCode:u,register:d}){let p=(0,b.useRef)(null),h=(0,b.useRef)(null),{locale:g}=f(),_=(0,b.useRef)(g);_.current=g;let T=n?JSON.stringify(n):``,E=(0,b.useRef)(c===!1);(0,b.useEffect)(()=>{let c=p.current;if(!c)return;let u=new v({theme:l(),fontFamily:`"SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace`,fontSize:13,cursorBlink:!0,scrollback:8e3}),f=new y;u.loadAddon(f),u.open(c),h.current={term:u,fit:f},s&&u.focus();let g=()=>{u.options.theme=l()};window.addEventListener(m,g),u.attachCustomKeyEventHandler(e=>{if(e.type!==`keydown`)return!0;let t=e.key?.toLowerCase();if((e.ctrlKey||e.metaKey)&&t===`v`)return!1;if(e.ctrlKey&&!e.shiftKey&&!e.altKey&&t===`c`&&u.hasSelection()){let e=u.textarea;return e&&(e.value=u.getSelection(),e.select()),!1}return!0});let b=d(e,t,{write:e=>u.write(w(e).clean),dispose:()=>u.dispose()}),C=()=>{try{f.fit(),r({type:`terminal_resize`,terminalId:t,conversationId:e,cols:u.cols,rows:u.rows})}catch{}},T=requestAnimationFrame(()=>{try{f.fit()}catch{}E.current||r(n?{type:`run_command`,terminalId:t,conversationId:e,command:n,cols:u.cols,rows:u.rows}:{type:`terminal_create`,terminalId:t,title:a,locale:_.current,agentBash:o,conversationId:e,cwd:i,cols:u.cols,rows:u.rows})}),D=u.onData(n=>{r({type:`terminal_input`,terminalId:t,conversationId:e,data:n})}),O=u.element,k=null,A=0,j=0,M=0,N=!1,P=e=>{if(e.touches.length!==1){k=null,N=!1;return}let t=e.touches[0];k=t.identifier,A=t.clientX,j=t.clientY,M=t.clientY,N=!1},F=e=>{if(k===null||e.touches.length!==1)return;let t=e.touches[0];if(t.identifier!==k)return;if(!N){if(!x(t.clientX-A,t.clientY-j))return;N=!0}e.preventDefault();let n=S(M,t.clientY);M=t.clientY,n!==0&&typeof WheelEvent<`u`&&(c.querySelector(`.xterm-viewport`)??O)?.dispatchEvent(new WheelEvent(`wheel`,{bubbles:!0,cancelable:!0,deltaY:n,deltaMode:0,clientX:t.clientX,clientY:t.clientY}))},I=e=>{if(k!==null){if(e.touches.length>0){k=null,N=!1;return}k=null,N=!1}};O?.addEventListener(`touchstart`,P,{passive:!0}),O?.addEventListener(`touchmove`,F,{passive:!1}),O?.addEventListener(`touchend`,I),O?.addEventListener(`touchcancel`,I);let L=null;return typeof ResizeObserver<`u`&&(L=new ResizeObserver(()=>{c.offsetWidth>0&&c.offsetHeight>0&&C()}),L.observe(c)),()=>{cancelAnimationFrame(T),D.dispose(),O?.removeEventListener(`touchstart`,P),O?.removeEventListener(`touchmove`,F),O?.removeEventListener(`touchend`,I),O?.removeEventListener(`touchcancel`,I),window.removeEventListener(m,g),L?.disconnect(),b(),u.dispose(),h.current=null}},[e,t,T,d]),(0,b.useEffect)(()=>{if(!s)return;let n=requestAnimationFrame(()=>{let n=h.current;if(n){try{n.fit.fit(),r({type:`terminal_resize`,terminalId:t,conversationId:e,cols:n.term.cols,rows:n.term.rows})}catch{}n.term.focus()}});return()=>cancelAnimationFrame(n)},[s]);let{t:D}=f(),O=(0,b.useRef)(void 0);return(0,b.useEffect)(()=>{if(c===O.current||(O.current=c,c!==!1))return;let e=h.current;e&&e.term.write(`\r\n\x1b[90m${D(`exitBanner`,{code:u??``})}\x1b[0m\r\n`)},[c,t,u]),(0,C.jsx)(`div`,{ref:p,className:`term-xterm ${s?``:`hidden`}`})}var E={name:``,command:``,cwd:"${pwd}"};function D({chat:e,terminal:t,uiTerminalToolbar:n,onUiAction:l}){let f=h(),[m,v]=(0,b.useState)(null),[y,x]=(0,b.useState)(!1),[S,w]=(0,b.useState)(!1),[D,O]=(0,b.useState)(null),[k,A]=(0,b.useState)(E),[j,M]=(0,b.useState)(null),N=(0,b.useRef)(null),[P,F]=(0,b.useState)(!0),[I,L]=(0,b.useState)(null),[R,z]=(0,b.useState)(``);(0,b.useEffect)(()=>{e.terminals.length===0?v(null):e.terminals.some(e=>e.id===m)||v(e.terminals[e.terminals.length-1].id)},[e.terminals,m]),(0,b.useEffect)(()=>{e.terminalActiveId&&(v(e.terminalActiveId),x(!1))},[e.terminalActiveId]),(0,b.useEffect)(()=>()=>{N.current&&clearTimeout(N.current)},[]);let B=e.terminals.filter(e=>!e.agentBash),V=e.terminals.filter(e=>e.agentBash),H=n=>{if(!e.ready)return;let r=i(),a=e.activeConversationId||e.state?.conversationId||``;t.create({...n,id:r,conversationId:a,cols:n.cols??80,rows:n.rows??24,running:!0,exitCode:null}),v(r),x(!1)},U=()=>H({title:f(`terminalTitle`,{n:B.length+1}),cwd:e.state?.cwd??``}),W=n=>{let i=n.name||n.command,a=e.terminals.find(e=>e.title===i);if(a){t.restart(a.id),v(a.id),r({type:`run_command`,terminalId:a.id,conversationId:a.conversationId,command:n,cols:80,rows:24});return}H({title:i,cwd:e.state?.cwd??``,command:n})},G=n=>{let i=e.terminals.find(e=>e.id===n);if(i&&r({type:`terminal_kill`,terminalId:n,conversationId:i.conversationId}),t.close(n),m===n){let t=e.terminals.filter(e=>e.id!==n);v(t.length>0?t[t.length-1].id:null)}},K=e=>(0,C.jsxs)(`div`,{className:`term-tab ${e.id===m?`active`:``}`,children:[(0,C.jsxs)(`button`,{type:`button`,className:`term-tab-main`,title:`${e.cwd}${e.command?`\n> ${e.command.command}`:``}`,onClick:()=>{I||(v(e.id),x(!1))},children:[(0,C.jsx)(`span`,{className:`term-tab-dot ${e.running?`run`:`exit`}`}),(0,C.jsxs)(`span`,{className:`term-tab-title`,children:[I===e.id?(0,C.jsx)(`input`,{autoFocus:!0,className:`term-tab-rename-input`,value:R,placeholder:e.title,onClick:e=>e.stopPropagation(),onChange:e=>z(e.target.value),onKeyDown:t=>{if(t.stopPropagation(),t.key===`Enter`&&!t.nativeEvent.isComposing){let t=R.trim();t&&r({type:`rename_terminal`,terminalId:e.id,conversationId:e.conversationId,title:t}),L(null)}else t.key===`Escape`&&L(null)},onBlur:()=>L(null)}):e.title,!e.running&&(0,C.jsx)(`span`,{className:`term-tab-exit`,children:f(`exited`,{code:e.exitCode===null?``:` ${e.exitCode}`})})]})]}),(0,C.jsx)(`button`,{type:`button`,className:`term-tab-close term-tab-rename`,title:f(`renameTerminal`),onClick:t=>{t.stopPropagation(),z(e.title),L(e.id)},children:(0,C.jsx)(u,{})}),(0,C.jsx)(`button`,{type:`button`,className:`term-tab-close`,title:f(`closeTerminal`),onClick:()=>G(e.id),children:(0,C.jsx)(s,{})})]},e.id),q=()=>{w(!0),O(null),A(E)},J=t=>{let n=e.commands[t];n&&(w(!1),O(t),A({name:n.name,command:n.command,cwd:n.cwd??``}))},Y=()=>{w(!1),O(null)},X=()=>{let t=k.name.trim(),n=k.command.trim();if(!t||!n)return;let i={name:t,command:n,cwd:k.cwd.trim()||void 0},a=S?[...e.commands,i]:D===null?e.commands:e.commands.map((e,t)=>t===D?i:e);r({type:`save_commands`,commands:a}),Y()},Z=t=>{if(j===t){let n=e.commands.filter((e,n)=>n!==t);r({type:`save_commands`,commands:n}),M(null),N.current&&clearTimeout(N.current)}else M(t),N.current&&clearTimeout(N.current),N.current=setTimeout(()=>M(null),2500)},ee=S||D!==null,te=n===void 0?[`host:term-cmd-refresh`,`host:term-cmd-new`,`host:term-tab-new`].map(e=>({id:e,source:`host`})):n.filter(e=>!e.hidden),Q=(e,t)=>{let n=new Set(e);return te.filter(e=>n.has(e.id)||t&&e.source!==`host`)},$={"host:term-cmd-refresh":(0,C.jsx)(`button`,{type:`button`,className:`panel-refresh`,title:f(`rerun`),onClick:()=>r({type:`list_commands`}),children:(0,C.jsx)(p,{})}),"host:term-cmd-new":(0,C.jsx)(`button`,{type:`button`,className:`panel-new`,title:f(`newCommand`),onClick:q,children:(0,C.jsx)(o,{})}),"host:term-tab-new":(0,C.jsx)(`button`,{type:`button`,className:`panel-new`,title:f(`newTerminal`),onClick:U,children:(0,C.jsx)(o,{})})};return(0,C.jsxs)(`div`,{className:`terminal-view`,children:[(0,C.jsxs)(`aside`,{className:`term-side term-commands ${y?`open`:``}`,children:[(0,C.jsxs)(`div`,{className:`panel-header`,children:[(0,C.jsx)(`span`,{className:`panel-title`,children:f(`commands`)}),(0,C.jsx)(`div`,{className:`panel-header-actions`,children:g(Q([`host:term-cmd-refresh`,`host:term-cmd-new`],!1),$,l)})]}),(0,C.jsx)(`div`,{className:`panel-body`,children:ee?(0,C.jsxs)(`div`,{className:`cmd-form`,children:[(0,C.jsx)(`label`,{htmlFor:`cmd-name`,children:f(`name`)}),(0,C.jsx)(`input`,{id:`cmd-name`,className:`cmd-input`,value:k.name,placeholder:f(`exampleName`),autoFocus:!0,onChange:e=>A({...k,name:e.target.value})}),(0,C.jsx)(`label`,{htmlFor:`cmd-command`,children:f(`command`)}),(0,C.jsx)(`input`,{id:`cmd-command`,className:`cmd-input`,value:k.command,placeholder:f(`exampleCommand`),onChange:e=>A({...k,command:e.target.value}),onKeyDown:e=>{e.key===`Enter`&&!e.nativeEvent.isComposing&&X()}}),(0,C.jsxs)(`label`,{htmlFor:`cmd-cwd`,children:[f(`directory`),` `,(0,C.jsx)(`span`,{className:`cmd-hint`,children:f(`cwdHint`)})]}),(0,C.jsx)(`input`,{id:`cmd-cwd`,className:`cmd-input`,value:k.cwd,placeholder:"${pwd}",onChange:e=>A({...k,cwd:e.target.value}),onKeyDown:e=>{e.key===`Enter`&&!e.nativeEvent.isComposing&&X()}}),(0,C.jsxs)(`div`,{className:`cmd-form-actions`,children:[(0,C.jsx)(`button`,{type:`button`,className:`btn`,onClick:Y,children:f(`cancel`)}),(0,C.jsx)(`button`,{type:`button`,className:`btn primary`,disabled:!k.name.trim()||!k.command.trim(),onClick:X,children:f(`save`)})]})]}):(0,C.jsxs)(C.Fragment,{children:[e.commands.length===0&&(0,C.jsx)(`div`,{className:`panel-empty`,children:f(`noCommands`)}),e.commands.map((e,t)=>(0,C.jsxs)(`div`,{className:`cmd-item`,children:[(0,C.jsx)(`button`,{type:`button`,className:`cmd-run`,title:f(`clickToRun`),onClick:()=>W(e),children:(0,C.jsx)(a,{})}),(0,C.jsxs)(`button`,{type:`button`,className:`cmd-main`,title:f(`clickToRun`),onClick:()=>W(e),children:[(0,C.jsx)(`span`,{className:`cmd-name`,children:e.name}),(0,C.jsx)(`span`,{className:`cmd-command`,children:e.command}),e.cwd&&(0,C.jsx)(`span`,{className:`cmd-cwd`,children:e.cwd})]}),(0,C.jsx)(`button`,{type:`button`,className:`cmd-act`,title:f(`edit`),onClick:()=>J(t),children:(0,C.jsx)(u,{})}),(0,C.jsx)(`button`,{type:`button`,className:`cmd-act del ${j===t?`confirm`:``}`,title:f(`delete`),onClick:()=>Z(t),children:j===t?f(`confirmQ`):(0,C.jsx)(c,{})})]},t))]})}),(0,C.jsxs)(`div`,{className:`term-tabs-block`,children:[(0,C.jsxs)(`div`,{className:`panel-header`,children:[(0,C.jsx)(`span`,{className:`panel-title`,children:f(`terminal`)}),g(Q([`host:term-tab-new`],!0),$,l)]}),(0,C.jsxs)(`div`,{className:`panel-body`,children:[e.terminals.length===0&&(0,C.jsx)(`div`,{className:`panel-empty`,children:f(`noTerminal`)}),B.map(K),V.length>0&&(0,C.jsxs)(`div`,{className:`term-folder`,children:[(0,C.jsxs)(`button`,{type:`button`,className:`term-folder-header ${P?`open`:``}`,title:f(`aiBashGroup`),onClick:()=>F(e=>!e),children:[(0,C.jsx)(`span`,{className:`term-folder-caret`,children:P?`▾`:`▸`}),(0,C.jsx)(`span`,{className:`term-folder-title`,children:f(`aiBashGroup`)}),(0,C.jsx)(`span`,{className:`term-folder-count`,children:V.length})]}),P&&(0,C.jsx)(`div`,{className:`term-folder-body`,children:V.map(K)})]})]})]})]}),(0,C.jsxs)(`div`,{className:`term-main`,children:[y&&(0,C.jsx)(`div`,{className:`drawer-backdrop`,onClick:()=>x(!1)}),(0,C.jsx)(`button`,{type:`button`,className:`term-side-toggle`,title:f(`commands`),onClick:()=>x(e=>!e),children:(0,C.jsx)(_,{})}),e.terminals.length===0?(0,C.jsxs)(`div`,{className:`term-empty`,children:[(0,C.jsx)(d,{className:`term-empty-icon`}),(0,C.jsx)(`div`,{className:`term-empty-title`,children:f(`builtinTerminal`)}),(0,C.jsx)(`div`,{className:`term-empty-sub`,children:f(`termEmptySub`)})]}):e.terminals.map(e=>(0,C.jsx)(T,{conversationId:e.conversationId,terminalId:e.id,command:e.command,cwd:e.cwd,title:e.title,agentBash:e.agentBash,active:e.id===m,running:e.running,exitCode:e.exitCode,register:t.register},`${e.conversationId}:${e.id}`))]})]})}export{D as TerminalPanel};