pi-web-ui 0.72.0 → 0.74.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.
@@ -1742,7 +1742,9 @@ export class DshClientSession {
1742
1742
  });
1743
1743
  }
1744
1744
  }
1745
- async dismissConversation(id) {
1745
+ async dismissConversation(id, _withFinishedSubagents, force) {
1746
+ // DSH 引擎无第一方子代理:withFinishedSubagents 恒为 no-op(签名与标准引擎对齐)。
1747
+ // force 放行 active/终端限制;运行中仍拒绝(DSH 单 runtime 无法单独 abort 一个对话,请先点停止)。
1746
1748
  const conv = this.convs.get(id);
1747
1749
  if (!conv) {
1748
1750
  this.emit({
@@ -1753,7 +1755,7 @@ export class DshClientSession {
1753
1755
  });
1754
1756
  return;
1755
1757
  }
1756
- if (id === this.activeId) {
1758
+ if (id === this.activeId && !force) {
1757
1759
  this.emit({
1758
1760
  type: "notice",
1759
1761
  level: "warning",
@@ -1775,7 +1777,7 @@ export class DshClientSession {
1775
1777
  });
1776
1778
  return;
1777
1779
  }
1778
- if (conv.terminals.list().length > 0) {
1780
+ if (conv.terminals.list().length > 0 && !force) {
1779
1781
  this.emit({
1780
1782
  type: "notice",
1781
1783
  level: "warning",
@@ -1784,10 +1786,37 @@ export class DshClientSession {
1784
1786
  });
1785
1787
  return;
1786
1788
  }
1789
+ // force + active:先让出 active(切到其他对话或新建),再移除。
1790
+ if (id === this.activeId) {
1791
+ const other = [...this.convs.values()].find((c) => c.id !== id);
1792
+ if (other)
1793
+ await this.switchConversation(other.id);
1794
+ else
1795
+ await this.newChat();
1796
+ if (id === this.activeId) {
1797
+ this.emit({
1798
+ type: "notice",
1799
+ level: "warning",
1800
+ text: `当前对话「${conv.title}」暂时无法移出(无法创建接替对话)`,
1801
+ textEn: `Cannot dismiss the active conversation "${conv.title}" right now (no replacement chat available)`,
1802
+ });
1803
+ return;
1804
+ }
1805
+ }
1787
1806
  this.removeConversation(id);
1788
1807
  this.emitConversations();
1789
1808
  this.flushSnapshot();
1790
1809
  }
1810
+ /** DSH 引擎无第一方子代理(emitConversations 恒 isSubagent:false):批量
1811
+ * 关闭退化为空操作提示,保持与 pi 引擎同一 wire 行为。 */
1812
+ async dismissFinishedSubagents(_parentId) {
1813
+ this.emit({
1814
+ type: "notice",
1815
+ level: "info",
1816
+ text: "没有可关闭的已结束子代理",
1817
+ textEn: "No finished subagents to dismiss",
1818
+ });
1819
+ }
1791
1820
  /** 切换会话:读 JSONL 回放 → 新建 conversation(同一 sessionId 续聊)。 */
1792
1821
  async switchSession(path) {
1793
1822
  try {
@@ -3029,6 +3058,14 @@ export class DshClientSession {
3029
3058
  async listModelsConfig() {
3030
3059
  this.emit({ type: "models_config", providers: [] });
3031
3060
  }
3061
+ async reloadModelsConfig() {
3062
+ this.emit({
3063
+ type: "notice",
3064
+ level: "warning",
3065
+ text: "DSH 引擎不支持自定义 provider",
3066
+ textEn: "The DSH engine does not support custom providers",
3067
+ });
3068
+ }
3032
3069
  async saveModelConfig(_providerId, _config) {
3033
3070
  this.emit({
3034
3071
  type: "notice",
@@ -756,7 +756,10 @@ wss.on("connection", (ws) => {
756
756
  void cs.renameConversation(msg.id, msg.name);
757
757
  break;
758
758
  case "dismiss_conversation":
759
- void cs.dismissConversation(msg.id);
759
+ void cs.dismissConversation(msg.id, msg.withFinishedSubagents, msg.force);
760
+ break;
761
+ case "dismiss_finished_subagents":
762
+ void cs.dismissFinishedSubagents(msg.parentId);
760
763
  break;
761
764
  case "switch_session":
762
765
  void cs.switchSession(msg.path);
@@ -838,6 +841,9 @@ wss.on("connection", (ws) => {
838
841
  case "list_models_config":
839
842
  void cs.listModelsConfig();
840
843
  break;
844
+ case "reload_models_config":
845
+ void cs.reloadModelsConfig();
846
+ break;
841
847
  case "save_model_config":
842
848
  void cs.saveModelConfig(msg.providerId, msg.config);
843
849
  break;
@@ -57,6 +57,70 @@ function stripJsonComments(src) {
57
57
  }
58
58
  return out;
59
59
  }
60
+ /** Merge a UI-submitted provider config into the existing models.json entry.
61
+ *
62
+ * 表单(`UiProviderConfig`)只承载 UI 认识的字段:provider 级 name/api/baseUrl/
63
+ * apiKey/authHeader,模型级 id/name/reasoning/input/contextWindow/maxTokens。
64
+ * models.json 里还可能有 UI 不认识的字段——provider 级 headers(浏览器拿不到,
65
+ * 见 listModelsConfig)、模型级 api/baseUrl/cost/compat/thinkingLevelMap(手写或
66
+ * 脚本写入,pi-ai 靠它们决定请求地址与推理格式)。按表单整体重建条目会把这些字段
67
+ * 静默抹掉,可能把可用的配置改坏:模型级 baseUrl 丢失后会回退到对该 api 适配器
68
+ * 无效的地址(例如 opencode-go 的 anthropic baseUrl),请求直接打到不存在的路径。
69
+ *
70
+ * 所以这里以已有条目为底、表单字段覆盖:表单没提到的字段原样保留,表单清空的可选
71
+ * 字段才真正删除;`models` 仍是「表单即全集」——表单里删掉的 id 会被移除。
72
+ */
73
+ export function mergeProviderConfigEntry(prevEntry, config, models) {
74
+ const prevModels = new Map();
75
+ if (Array.isArray(prevEntry?.models)) {
76
+ for (const entry of prevEntry.models) {
77
+ if (!entry || typeof entry !== "object")
78
+ continue;
79
+ const id = entry.id;
80
+ if (typeof id === "string" && id.trim())
81
+ prevModels.set(id.trim(), entry);
82
+ }
83
+ }
84
+ const mergedModels = models.map((model) => {
85
+ // 旧条目同 id 的字段(api/baseUrl/cost/compat/…)先铺底,表单字段覆盖。
86
+ const merged = { ...(prevModels.get(model.id) ?? {}), id: model.id };
87
+ if (model.name?.trim())
88
+ merged.name = model.name.trim();
89
+ else
90
+ delete merged.name;
91
+ if (model.reasoning)
92
+ merged.reasoning = true;
93
+ else
94
+ delete merged.reasoning;
95
+ if (model.input?.length)
96
+ merged.input = model.input;
97
+ else
98
+ delete merged.input;
99
+ if (model.contextWindow)
100
+ merged.contextWindow = Number(model.contextWindow);
101
+ else
102
+ delete merged.contextWindow;
103
+ if (model.maxTokens)
104
+ merged.maxTokens = Number(model.maxTokens);
105
+ else
106
+ delete merged.maxTokens;
107
+ return merged;
108
+ });
109
+ const mergedEntry = { ...prevEntry };
110
+ const apply = (key, value) => {
111
+ if (value === undefined)
112
+ delete mergedEntry[key];
113
+ else
114
+ mergedEntry[key] = value;
115
+ };
116
+ apply("name", config.name?.trim() || undefined);
117
+ apply("api", config.api?.trim() || undefined);
118
+ apply("baseUrl", config.baseUrl?.trim() || undefined);
119
+ apply("apiKey", config.apiKey?.trim() || undefined);
120
+ apply("authHeader", config.authHeader ? true : undefined);
121
+ mergedEntry.models = mergedModels;
122
+ return mergedEntry;
123
+ }
60
124
  /** Numeric metadata value (NaN/string "unknown" → undefined). */
61
125
  function numMeta(v) {
62
126
  return typeof v === "number" && Number.isFinite(v) ? v : undefined;
@@ -821,6 +885,31 @@ export class ModelAdminService {
821
885
  });
822
886
  this.host.emit({ type: "models_config", providers: list });
823
887
  }
888
+ /** Re-read models.json from disk (hand/script edits outside the UI) and
889
+ * repush — same refresh tail that save_model_config runs. */
890
+ async reloadModelsConfig() {
891
+ try {
892
+ await this.host.modelRuntime().refresh();
893
+ this.host.invalidatePiConfig();
894
+ await this.listModelsConfig();
895
+ await this.host.pushModels();
896
+ this.host.emit({
897
+ type: "notice",
898
+ level: "info",
899
+ text: "🔄 已从磁盘重新加载模型配置",
900
+ textEn: "🔄 Reloaded model config from disk",
901
+ });
902
+ }
903
+ catch (err) {
904
+ this.host.emit({
905
+ type: "notice",
906
+ level: "error",
907
+ text: `重新加载模型配置失败:${err.message}`,
908
+ textEn: `Failed to reload model config: ${err.message}`,
909
+ });
910
+ }
911
+ this.host.flushSnapshot();
912
+ }
824
913
  /** Numeric metadata value (NaN/string "unknown" → undefined). */
825
914
  static numMeta(v) {
826
915
  return typeof v === "number" && Number.isFinite(v) ? v : undefined;
@@ -1016,7 +1105,11 @@ export class ModelAdminService {
1016
1105
  const { providers } = this.readModelsConfig();
1017
1106
  // models.json 原始形状是 Record<string, unknown>——按已保存条目的结构断言
1018
1107
  const saved = providers[pid];
1019
- if (!saved?.baseUrl?.trim()) {
1108
+ // 纯覆盖条目(只改 models,没有 provider 级 baseUrl)回退到运行时
1109
+ // 已知的地址——内置 provider 的 baseUrl 本来就不在 models.json 里。
1110
+ // 注意只拿来探测用,不写回磁盘:保持条目仍是纯覆盖。
1111
+ const baseUrl = saved?.baseUrl?.trim() || (this.host.modelRuntime().getProvider(pid)?.baseUrl ?? "").trim() || undefined;
1112
+ if (!saved || !baseUrl) {
1020
1113
  this.host.emit({
1021
1114
  type: "notice",
1022
1115
  level: "warning",
@@ -1025,7 +1118,7 @@ export class ModelAdminService {
1025
1118
  });
1026
1119
  return done(false, { error: "provider missing or no baseUrl" });
1027
1120
  }
1028
- const fetched = await ModelAdminService.probeModelsEndpoint(saved.baseUrl, saved.apiKey, saved.authHeader === true ? true : undefined, saved.api, saved.headers, lang);
1121
+ const fetched = await ModelAdminService.probeModelsEndpoint(baseUrl, saved.apiKey, saved.authHeader === true ? true : undefined, saved.api, saved.headers, lang);
1029
1122
  // Merge: manual values win; fetched fills blanks and appends new ids.
1030
1123
  const prev = new Map((saved.models ?? []).map((m) => [m.id, m]));
1031
1124
  let added = 0;
@@ -1106,18 +1199,9 @@ export class ModelAdminService {
1106
1199
  }
1107
1200
  try {
1108
1201
  const { providers } = this.readModelsConfig();
1109
- // headers never reach the browser, so the incoming config can't carry
1110
- // them — preserve the previously stored values when they are absent.
1111
- const prevHeaders = providers[pid]?.headers;
1112
- providers[pid] = {
1113
- ...(config.name?.trim() ? { name: config.name.trim() } : {}),
1114
- ...(config.api?.trim() ? { api: config.api.trim() } : {}),
1115
- ...(config.baseUrl?.trim() ? { baseUrl: config.baseUrl.trim() } : {}),
1116
- ...(config.apiKey?.trim() ? { apiKey: config.apiKey.trim() } : {}),
1117
- ...(config.authHeader ? { authHeader: true } : {}),
1118
- ...(prevHeaders && Object.keys(prevHeaders).length > 0 ? { headers: prevHeaders } : {}),
1119
- models,
1120
- };
1202
+ // 合并而不是重建:UI 认识之外的字段(provider headers、模型级 api/
1203
+ // baseUrl/cost/compat/thinkingLevelMap)必须原样保留,见 mergeProviderConfigEntry。
1204
+ providers[pid] = mergeProviderConfigEntry(providers[pid], config, models);
1121
1205
  mkdirSync(this.host.agentDir, { recursive: true });
1122
1206
  writeFileSync(this.modelsConfigPath(), JSON.stringify({ providers }, null, 2) + "\n");
1123
1207
  // Allow a custom models.json entry to reuse the provider credential
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Remote-catalog merge patch for @earendil-works/pi-coding-agent.
3
+ *
4
+ * The SDK composes each built-in provider's model list as
5
+ * mergeModels(builtinStaticCatalog, pi.devRemoteCatalog)
6
+ * which is a UNION: same-id entries get replaced by the newer pi.dev row, new
7
+ * ids are appended, but stale built-in models are kept forever (and the UI
8
+ * reports "新增 N 个模型" when the overlay grows).
9
+ *
10
+ * pi-web-ui wants the picker to mirror the OFFICIAL remote catalog exactly —
11
+ * an "ensure latest, no merge, no 'new N' noise" behavior. This module rewrites
12
+ * the installed copy of remote-catalog-provider.js so the remote overlay
13
+ * REPLACES the built-in catalog entirely whenever pi.dev data is present:
14
+ * getModels: () => (dynamicModels.length > 0 ? dynamicModels : provider.getModels())
15
+ * (the built-in catalog only serves as fallback while no remote data has been
16
+ * received yet, so the list can never be empty).
17
+ *
18
+ * Same pattern as patch-node-pty.ts: idempotent (checks before rewriting),
19
+ * best-effort (a read-only node_modules or an SDK version whose source no
20
+ * longer matches the search strings just skips the patch and keeps the SDK's
21
+ * default union semantics — never a crash). MUST be imported before the SDK
22
+ * modules are first loaded (agent-service.ts imports this first).
23
+ */
24
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
25
+ import { fileURLToPath } from "node:url";
26
+ import { dirname, join } from "node:path";
27
+ /** Old fragment that performs the union merge (single occurrence in the file). */
28
+ const OLD = "getModels: () => mergeModels(provider.getModels(), dynamicModels),";
29
+ /** Replacement: whole-replace with the remote catalog when available. */
30
+ const NEW = "getModels: () => (dynamicModels.length > 0 ? dynamicModels : provider.getModels()),";
31
+ /** Absolute path of the installed remote-catalog-provider.js, or null. */
32
+ function remoteCatalogFile() {
33
+ try {
34
+ // import.meta.resolve honors the package "exports" (import condition) → dist/index.js;
35
+ // remote-catalog-provider.js lives next to it in dist/core/.
36
+ const entry = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
37
+ return join(dirname(entry), "core", "remote-catalog-provider.js");
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ function applyPatch() {
44
+ try {
45
+ const file = remoteCatalogFile();
46
+ if (!file || !existsSync(file))
47
+ return;
48
+ const src = readFileSync(file, "utf8");
49
+ if (src.includes(NEW))
50
+ return; // already patched
51
+ if (!src.includes(OLD))
52
+ return; // unexpected SDK content — leave alone
53
+ writeFileSync(file, src.replace(OLD, NEW), "utf8");
54
+ }
55
+ catch {
56
+ // best-effort — without the patch the SDK keeps its default union merge
57
+ }
58
+ }
59
+ applyPatch();
@@ -68,8 +68,14 @@ export function withSubagentOwner(host, ownerId) {
68
68
  * 子代理工具集(注册进每个会话的 customTools,供主 agent 驱动子代理)。
69
69
  * 用 `subagent_*` 前缀命名,避免与第三方 pi-subagents 的
70
70
  * `Agent`/`get_subagent_result`/`steer_subagent` 冲突。
71
+ *
72
+ * `selfConvId`(可选):这套工具所注册进的会话 convId。`subagent_wait_all`
73
+ * 永远排除调用者自身——子代理会话上同样注册了全套工具,不传 runIds 时
74
+ * 「全部」会含它自己,而它正在执行本工具(streaming=true),不排除就是
75
+ * 自己等自己、永远到超时(self-wait deadlock)。主会话调用时传它自己的
76
+ * 普通对话 id 即可(不在子代理列表里,delete 是 no-op)。
71
77
  */
72
- export function makeSubagentTools(host, lang) {
78
+ export function makeSubagentTools(host, lang, selfConvId) {
73
79
  const getLang = lang ?? host.lang ?? (() => "en");
74
80
  const text = (t, details = {}) => ({
75
81
  content: [{ type: "text", text: t }],
@@ -232,11 +238,13 @@ export function makeSubagentTools(host, lang) {
232
238
  label: "Wait for subagents",
233
239
  description: bilingual("Wait for multiple subagents to finish at once (blocks this round until all reach a terminal state or time out), " +
234
240
  "then summarize each result/error — no need to poll subagent_get_result. Pass runIds for specific subagents " +
235
- "(convIds returned by subagent_spawn); omit = wait for all currently running ones. On timeout or abort of this " +
236
- "round, returns the remaining unfinished list; call again to continue waiting. " +
241
+ "(convIds returned by subagent_spawn); omit = wait for all currently running ones. The calling session itself " +
242
+ "is never waited on (a subagent calling this without runIds won't deadlock on itself). " +
243
+ "On timeout or abort of this round, returns the remaining unfinished list; call again to continue waiting. " +
237
244
  "Good for: collecting parallel subagents.", "一次性等待多个子代理全部完成(阻塞本回合直到它们都到达终态或超时),然后汇总返回每个的结果/错误——" +
238
245
  "不用反复调 subagent_get_result 轮询。传 runIds 指定要等的子代理(subagent_spawn 返回的 convId);" +
239
- "不传 = 等当前全部运行中的子代理。超时或本轮被中止时返回剩余未完成名单,可再次调用继续等。" +
246
+ "不传 = 等当前全部运行中的子代理。调用者自身永不计入等待(子代理不传 runIds 时不会等自己)。" +
247
+ "超时或本轮被中止时返回剩余未完成名单,可再次调用继续等。" +
240
248
  "适合:并行派发多个子代理后收口。"),
241
249
  promptSnippet: "wait for multiple subagents to finish (no polling) and get all results",
242
250
  parameters: Type.Object({
@@ -251,6 +259,15 @@ export function makeSubagentTools(host, lang) {
251
259
  }),
252
260
  execute: async (_id, p, signal) => {
253
261
  const wanted = new Set(p.runIds && p.runIds.length > 0 ? p.runIds : host.listSubagents().map((r) => r.convId));
262
+ // 调用者自身永不等待:子代理调本工具时它自己正在 streaming,不排除
263
+ // 就是自己等自己、永远到超时(self-wait deadlock)。主会话的普通
264
+ // 对话 id 不在子代理列表里,delete 是 no-op。
265
+ if (selfConvId)
266
+ wanted.delete(selfConvId);
267
+ if (wanted.size === 0) {
268
+ const emptyLang = getLang();
269
+ return text(pick(emptyLang, "没有需要等待的子代理(调用者自身不计入等待)。", "No subagents to wait for (the calling session itself is never waited on).", "subagents.wait.empty"));
270
+ }
254
271
  const timeoutMs = Math.min(Math.max(p.timeoutSeconds ?? 600, 1), Math.floor(WAIT_CAP_MS / 1000)) * 1000;
255
272
  const waitStart = Date.now();
256
273
  const deadline = waitStart + timeoutMs;
@@ -327,6 +344,36 @@ export function makeSubagentTools(host, lang) {
327
344
  function shortId(id) {
328
345
  return id.slice(0, 8);
329
346
  }
347
+ /**
348
+ * 收集 root 的传递子代理后代 id(parentId 链向上能走到 root 的;含嵌套的嵌套)。
349
+ * root 自身不含;非子代理对话不含(普通对话不参与子代理清理口径)。
350
+ * parentId 环(理论上不应出现)按 visited 截断,不会死循环。
351
+ * 纯函数:后端 dismiss 流程与单测共用(前端左栏按同样口径镜像实现,见
352
+ * LeftPanel finishedSubagentCount)。
353
+ */
354
+ export function collectSubagentDescendantIds(items, rootId) {
355
+ const byId = new Map(items.map((c) => [c.id, c]));
356
+ const out = [];
357
+ for (const c of items) {
358
+ if (!c.isSubagent || c.id === rootId)
359
+ continue;
360
+ let cur = c;
361
+ const seen = new Set();
362
+ while (cur?.parentId) {
363
+ if (cur.parentId === rootId) {
364
+ out.push(c.id);
365
+ break;
366
+ }
367
+ if (seen.has(cur.parentId))
368
+ break;
369
+ seen.add(cur.parentId);
370
+ cur = byId.get(cur.parentId);
371
+ if (!cur)
372
+ break;
373
+ }
374
+ }
375
+ return out;
376
+ }
330
377
  /** 人类可读的终态判定:报错 > 中止 > done > running。 */
331
378
  function subagentVerdict(r, lang = "en") {
332
379
  if (r.error)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-ui",
3
- "version": "0.72.0",
3
+ "version": "0.74.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": {
@@ -43,6 +43,9 @@
43
43
  --tooltip-bg: #232733;
44
44
  --code-bg: #0b0d12;
45
45
  --code-text: #c9d1d9;
46
+ --code-border: var(--border-soft);
47
+ --code-pad: 8px 10px;
48
+ --card-body-pad: 4px 14px 12px;
46
49
  --err-text: #f0a5a5;
47
50
  --red-text: #fca5a5;
48
51
  --amber-text: #fcd34d;
package/themes/dazzle.css CHANGED
@@ -43,6 +43,9 @@
43
43
  --tooltip-bg: #232733;
44
44
  --code-bg: #0b0d12;
45
45
  --code-text: #c9d1d9;
46
+ --code-border: var(--border-soft);
47
+ --code-pad: 8px 10px;
48
+ --card-body-pad: 4px 14px 12px;
46
49
  --err-text: #f0a5a5;
47
50
  --red-text: #fca5a5;
48
51
  --amber-text: #fcd34d;
@@ -43,6 +43,9 @@
43
43
  --tooltip-bg: #232733;
44
44
  --code-bg: #0b0d12;
45
45
  --code-text: #c9d1d9;
46
+ --code-border: var(--border-soft);
47
+ --code-pad: 8px 10px;
48
+ --card-body-pad: 4px 14px 12px;
46
49
  --err-text: #f0a5a5;
47
50
  --red-text: #fca5a5;
48
51
  --amber-text: #fcd34d;
@@ -11,7 +11,9 @@
11
11
  *
12
12
  * 全透后 hover/浮动反馈不能依赖容器色(已是 0%),本文件尾段把这些实色
13
13
  * hover 统一改成 25% 半透底(折叠摘要、展开条、折叠键、复制键、滚到底按钮);
14
- * 强调色 hover(accent-soft)与语义按钮(发送/停止/杀进程)不受影响。
14
+ * 强调色 hover(accent-soft)与大部分语义按钮(发送/杀进程)不受影响;
15
+ * 例外是 bash 工具卡的「停止」键(.toolcall-kill):红底红字在全透下是一块脏红,
16
+ * 改成描边式(透明底 + 半透红描边 + 亮红字)。
15
17
  * 下拉弹窗(声音/语言/主题/模型/思考强度等 .dd-menu)保持实色
16
18
  * (--menu-bg: var(--bg-elev2)),密集列表不随界面透图。
17
19
  * 配壁纸用最佳;不配壁纸时界面直接透出 --bg 纯色(= 默认暗色,无差别)。
@@ -59,12 +61,25 @@
59
61
  控件 / 输入框 / 消息区)直接变 transparent;终端 --term-bg 上面已设
60
62
  transparent。想留一点点玻璃感就把这里提到 10~15%。---- */
61
63
  --wallpaper-panel-alpha: 0%;
64
+ /* 工具参数 / 工具输出 / 终端行(.toolcall-args pre / .toolcall-output pre /
65
+ .termline):全透下这三处无容器色(--chip-bg = 0%),实色边框与外凸的
66
+ 内边距只会把文字推出对齐线并横向切断壁纸,这里一并去掉。
67
+ markdown 围栏代码块 .codeblock pre 仍保留边框与内边距。 */
68
+ --code-border: transparent;
69
+ --code-pad: 0;
62
70
  /* 下拉弹窗(声音/语言/主题/模型/思考强度):实色底保密集列表可读 */
63
71
  --menu-bg: var(--bg-elev2);
64
72
  /* 压暗加重:全透后没有容器色兜底,文字只靠压暗保可读 */
65
73
  --bg-image-dim: 0.85;
66
74
  }
67
75
 
76
+ /* 全透:工具卡内部不再保留重复的终端图标。无容器色兜底时(--chip-bg = 0%),绿色
77
+ 终端图标只剩视觉噪音(卡头已有工具图标)。卡片内边距仍走 --card-body-pad
78
+ (与思考块 .thinking-body 同值),文字左缘与思考块对齐。 */
79
+ .termline-icon {
80
+ display: none;
81
+ }
82
+
68
83
  /* hover / 浮动静置反馈:0% 容器色下这些实色 hover 必消失,统一给 25% 半透
69
84
  底(仍透图、可辨反馈)。强调色 hover(accent-soft)与语义按钮不受影响;
70
85
  下拉弹窗已在实色菜单内,其 hover 保留 styles.css 默认实色。 */
@@ -75,4 +90,19 @@
75
90
  .empty-templates-reset:hover,
76
91
  .scroll-bottom {
77
92
  background: color-mix(in srgb, var(--bg-elev) 25%, transparent);
78
- }
93
+ }
94
+
95
+ /* bash 工具卡的「停止」键:全透下不做实底。容器色为 0% 时,--red-soft 底 +
96
+ 实红描边在壁纸上是一块洗不干净的脏红,改成「透明底 + 半透红描边 + 亮红字」——
97
+ 与卡片里其他描边元素同一种克制感;hover 才落一层 78% 半透红(仍透图,
98
+ 不成为实色块)。 */
99
+ .toolcall-kill {
100
+ background: transparent;
101
+ border-color: color-mix(in srgb, var(--red) 55%, transparent);
102
+ color: color-mix(in srgb, var(--red) 88%, #fff);
103
+ }
104
+ .toolcall-kill:hover {
105
+ background: color-mix(in srgb, var(--red) 78%, transparent);
106
+ border-color: var(--red);
107
+ color: #fff;
108
+ }
package/themes/white.css CHANGED
@@ -43,6 +43,9 @@
43
43
  --tooltip-bg: #ffffff;
44
44
  --code-bg: #f6f8fa;
45
45
  --code-text: #1f2937;
46
+ --code-border: var(--border-soft);
47
+ --code-pad: 8px 10px;
48
+ --card-body-pad: 4px 14px 12px;
46
49
  --err-text: #dc2626;
47
50
  --red-text: #dc2626;
48
51
  --amber-text: #b45309;
@@ -1,4 +1,4 @@
1
- import{a as l,j as n}from"./markdown-Cpo0pNcR.js";import{u as P,b as O,T as M,a as W,F as Y,c as z,d as Z,e as H,f as ee,g as ne,h as te,i as se,r as ae}from"./index-CBZtMKV2.js";import{D as re,o as ie}from"./xterm-B96xOxS9.js";import"./react-CtudoG1_.js";function le(t){let c=null;return{clean:t.replace(/\r?\n?\[pi-term-exit:(-?\d+)\]\r?\n?/g,(a,x)=>(c=Number(x),`\r
1
+ import{a as l,j as n}from"./markdown-Cpo0pNcR.js";import{u as P,b as O,T as M,a as W,F as Y,c as z,d as Z,e as H,f as ee,g as ne,h as te,i as se,r as ae}from"./index-BplWuHvM.js";import{D as re,o as ie}from"./xterm-B96xOxS9.js";import"./react-CtudoG1_.js";function le(t){let c=null;return{clean:t.replace(/\r?\n?\[pi-term-exit:(-?\d+)\]\r?\n?/g,(a,x)=>(c=Number(x),`\r
2
2
  `)).replace(/\r?\n?\x1b\[90m\[(?:进程已退出,退出码 |Process exited with code )-?\d+\]\x1b\[0m\r?\n?/g,`\r
3
3
  `),exitCode:c}}function ce({conversationId:t,terminalId:c,command:p,cwd:a,title:x,active:u,running:v,exitCode:j,send:h,register:C}){const T=l.useRef(null),y=l.useRef(null),{locale:o}=P(),N=l.useRef(o);N.current=o;const E=p?JSON.stringify(p):"";l.useEffect(()=>{const m=T.current;if(!m)return;const r=new re({theme:O(),fontFamily:'"SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace',fontSize:13,cursorBlink:!0,scrollback:8e3}),b=new ie;r.loadAddon(b),r.open(m),y.current={term:r,fit:b},u&&r.focus();const g=()=>{r.options.theme=O()};window.addEventListener(M,g),r.attachCustomKeyEventHandler(d=>{if(d.type!=="keydown")return!0;const S=d.key?.toLowerCase();if((d.ctrlKey||d.metaKey)&&S==="v")return!1;if(d.ctrlKey&&!d.shiftKey&&!d.altKey&&S==="c"&&r.hasSelection()){const D=r.textarea;return D&&(D.value=r.getSelection(),D.select()),!1}return!0});const _=C(t,c,{write:d=>r.write(le(d).clean),dispose:()=>r.dispose()}),$=()=>{try{b.fit(),h({type:"terminal_resize",terminalId:c,conversationId:t,cols:r.cols,rows:r.rows})}catch{}},B=requestAnimationFrame(()=>{try{b.fit()}catch{}h(p?{type:"run_command",terminalId:c,conversationId:t,command:p,cols:r.cols,rows:r.rows}:{type:"terminal_create",terminalId:c,title:x,locale:N.current,conversationId:t,cwd:a,cols:r.cols,rows:r.rows})}),R=r.onData(d=>{h({type:"terminal_input",terminalId:c,conversationId:t,data:d})});let k=null;return typeof ResizeObserver<"u"&&(k=new ResizeObserver(()=>{m.offsetWidth>0&&m.offsetHeight>0&&$()}),k.observe(m)),()=>{cancelAnimationFrame(B),R.dispose(),window.removeEventListener(M,g),k?.disconnect(),_(),r.dispose(),y.current=null}},[t,c,E,h,C]),l.useEffect(()=>{if(!u)return;const m=requestAnimationFrame(()=>{const r=y.current;if(r){try{r.fit.fit(),h({type:"terminal_resize",terminalId:c,conversationId:t,cols:r.term.cols,rows:r.term.rows})}catch{}r.term.focus()}});return()=>cancelAnimationFrame(m)},[u]);const{t:F}=P(),f=l.useRef(void 0);return l.useEffect(()=>{if(v===f.current||(f.current=v,v!==!1))return;const m=y.current;m&&m.term.write(`\r
4
4
  \x1B[90m${F("exitBanner",{code:j??""})}\x1B[0m\r