pi-web-ui 0.96.0 → 0.97.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +69 -2
  2. package/README.md +26 -0
  3. package/README.zh-CN.md +25 -0
  4. package/bin/pi-web-ui.mjs +11 -2
  5. package/dist/server/agent-service.js +1195 -120
  6. package/dist/server/client-state.js +89 -1
  7. package/dist/server/compacted-history.js +146 -0
  8. package/dist/server/delegate-mode.js +149 -0
  9. package/dist/server/delegate-task.js +10 -10
  10. package/dist/server/dsh/dsh-agent-service.js +35 -2
  11. package/dist/server/dsh/dsh-client.js +3 -1
  12. package/dist/server/eval-tool.js +3 -1
  13. package/dist/server/goal-review-gate.js +263 -0
  14. package/dist/server/goal-service.js +1225 -445
  15. package/dist/server/index.js +79 -20
  16. package/dist/server/lsp-tool.js +13 -10
  17. package/dist/server/markers/builtins/todo.js +0 -2
  18. package/dist/server/mcp-bridge.js +13 -0
  19. package/dist/server/patch-tool.js +1 -0
  20. package/dist/server/patch-turn-end-boundary.js +77 -0
  21. package/dist/server/permission-preset.js +44 -0
  22. package/dist/server/plan-mode.js +376 -0
  23. package/dist/server/plugin-facilities.js +36 -3
  24. package/dist/server/plugin-installer.js +30 -0
  25. package/dist/server/plugins.js +6 -3
  26. package/dist/server/protocol-version.js +1 -1
  27. package/dist/server/serialize.js +11 -0
  28. package/dist/server/settings-service.js +20 -8
  29. package/dist/server/subagents.js +360 -408
  30. package/dist/server/text-sniff.js +15 -9
  31. package/dist/server/tool-manager.js +48 -4
  32. package/dist/server/update-check.js +11 -3
  33. package/dist/server/wait-subscription-scan.js +5 -1
  34. package/package.json +3 -3
  35. package/themes/zhupi-dark.css +79 -44
  36. package/themes/zhupi.css +79 -44
  37. package/web/dist/assets/{TerminalPanel-DDChcYOh.js → TerminalPanel-CITBqQrb.js} +1 -1
  38. package/web/dist/assets/index-DwToli0J.css +1 -0
  39. package/web/dist/assets/index-oI7UKOqJ.js +368 -0
  40. package/web/dist/index.html +2 -2
  41. package/web/dist/assets/index-Do9RgJC3.js +0 -366
  42. package/web/dist/assets/index-DryOsILO.css +0 -1
@@ -187,21 +187,27 @@ export function looksLikeText(buf) {
187
187
  }
188
188
  return control / Math.max(text.length, 1) < 0.02;
189
189
  }
190
- /** Decode bytes: strict UTF-8 first, falling back to GBK (Windows legacy
191
- * Chinese files), then latin1 as a last resort — so previews and inline
192
- * attachments never show mojibake for GBK/GB2312 encoded files. */
190
+ /** Decode bytes: strict UTF-8 first (retrying with up to 3 truncated tail
191
+ * bytes removed for excerpt/preview buffers cut mid-character), falling back
192
+ * to GBK (Windows legacy Chinese files), then latin1 as a last resort — so
193
+ * previews and inline attachments never show mojibake for GBK/GB2312 files
194
+ * nor for truncated UTF-8 files (issue #392). */
193
195
  export function decodeText(buf) {
194
- try {
195
- return new TextDecoder("utf-8", { fatal: true }).decode(buf);
196
- }
197
- catch {
196
+ for (let cut = 0; cut < 4; cut++) {
197
+ const slice = cut === 0 ? buf : buf.subarray(0, Math.max(0, buf.length - cut));
198
198
  try {
199
- return new TextDecoder("gbk").decode(buf);
199
+ return new TextDecoder("utf-8", { fatal: true }).decode(slice);
200
200
  }
201
201
  catch {
202
- return buf.toString("latin1");
202
+ // Tail byte might be cut mid-character; try trimming up to 3 bytes.
203
203
  }
204
204
  }
205
+ try {
206
+ return new TextDecoder("gbk").decode(buf);
207
+ }
208
+ catch {
209
+ return buf.toString("latin1");
210
+ }
205
211
  }
206
212
  /** Sniff an image MIME type from magic bytes (extension is only a hint).
207
213
  * Returns null when the bytes don't look like a known raster format —
@@ -25,8 +25,11 @@ export const TERMINAL_TOOL_NAMES = [
25
25
  "terminal_read",
26
26
  "terminal_wait",
27
27
  ];
28
- /** 第一方子代理工具(定义见 subagents.ts,逐个可关)。 */
29
- export const SUBAGENT_TOOL_NAMES = [
28
+ /** 第一方子代理工具(定义见 subagents.ts,统一为单 action 工具)。 */
29
+ export const SUBAGENT_TOOL_NAME = "subagent";
30
+ export const SUBAGENT_TOOL_NAMES = [SUBAGENT_TOOL_NAME];
31
+ /** 旧版 8 个独立子代理工具名(持久化配置迁移用)。 */
32
+ export const LEGACY_SUBAGENT_TOOL_NAMES = [
30
33
  "subagent_spawn",
31
34
  "subagent_get_result",
32
35
  "subagent_steer",
@@ -251,7 +254,7 @@ export const AGENT_TOOL_CATALOG = [
251
254
  },
252
255
  ];
253
256
  /** SDK 核心内置工具名(可被门控显式禁用或被预设白名单过滤)。 */
254
- export const CORE_BUILTIN_TOOL_NAMES = ["bash", "read", "edit", "write"];
257
+ export const CORE_BUILTIN_TOOL_NAMES = ["bash", "read", "edit", "write", "powershell", "ls", "grep", "find"];
255
258
  export function isCoreBuiltinTool(name) {
256
259
  return CORE_BUILTIN_TOOL_NAMES.includes(name);
257
260
  }
@@ -268,7 +271,11 @@ export function normalizeDisabledAgentTools(v) {
268
271
  const out = [];
269
272
  for (const x of v) {
270
273
  // 旧名迁移:markers_list → todo_list(改名前已关闭的用户保持关闭)。
271
- const name = x === LEGACY_MARKERS_LIST_TOOL_NAME ? MARKERS_LIST_TOOL_NAME : x;
274
+ let name = x === LEGACY_MARKERS_LIST_TOOL_NAME ? MARKERS_LIST_TOOL_NAME : x;
275
+ // 旧版子代理工具迁移:任意旧 subagent_* 关闭均迁移为关闭 subagent 工具。
276
+ if (typeof name === "string" && LEGACY_SUBAGENT_TOOL_NAMES.includes(name)) {
277
+ name = SUBAGENT_TOOL_NAME;
278
+ }
272
279
  if (typeof name === "string" && (KNOWN_NAMES.has(name) || isCoreBuiltinTool(name)) && !out.includes(name)) {
273
280
  out.push(name);
274
281
  }
@@ -363,7 +370,9 @@ export const PI_AGENT_PRESETS = [
363
370
  trust: "system",
364
371
  isDefault: true,
365
372
  name: "全功能",
373
+ nameEn: "Full access",
366
374
  description: "提供全部可用工具与扩展能力(默认)",
375
+ descriptionEn: "All available tools and extension capabilities (default)",
367
376
  order: 0,
368
377
  },
369
378
  {
@@ -371,7 +380,9 @@ export const PI_AGENT_PRESETS = [
371
380
  trust: "system",
372
381
  isDefault: false,
373
382
  name: "极简模式",
383
+ nameEn: "Minimal",
374
384
  description: "仅保留 bash 与 read;插件工具、技能名录与终端引导同步隐藏",
385
+ descriptionEn: "Keeps only bash and read; plugin tools, skill catalog and terminal guidance are hidden too",
375
386
  order: 1,
376
387
  },
377
388
  {
@@ -379,7 +390,9 @@ export const PI_AGENT_PRESETS = [
379
390
  trust: "system",
380
391
  isDefault: false,
381
392
  name: "代码开发",
393
+ nameEn: "Code development",
382
394
  description: "专注于代码读写与执行(bash, read, edit, write, edit_soft);插件工具与技能名录同步隐藏",
395
+ descriptionEn: "Focused on reading, writing and running code (bash, read, edit, write, edit_soft); plugin tools and skill catalog are hidden too",
383
396
  order: 2,
384
397
  },
385
398
  {
@@ -387,7 +400,9 @@ export const PI_AGENT_PRESETS = [
387
400
  trust: "system",
388
401
  isDefault: false,
389
402
  name: "只读分析",
403
+ nameEn: "Read-only analysis",
390
404
  description: "仅保留只读工具,禁止写操作;插件工具同步隐藏(读写未知,保守处理)",
405
+ descriptionEn: "Keeps only read-only tools and forbids writes; plugin tools are hidden too (read/write unknown, handled conservatively)",
391
406
  order: 3,
392
407
  },
393
408
  {
@@ -395,7 +410,9 @@ export const PI_AGENT_PRESETS = [
395
410
  trust: "system",
396
411
  isDefault: false,
397
412
  name: "纯对话",
413
+ nameEn: "Chat only",
398
414
  description: "无工具问答模式,模型不调用任何工具;插件工具与技能名录同步隐藏",
415
+ descriptionEn: "No-tools Q&A: the model never calls a tool; plugin tools and skill catalog are hidden too",
399
416
  order: 4,
400
417
  },
401
418
  ];
@@ -404,19 +421,45 @@ export const PI_PERMISSION_OPTIONS = [
404
421
  {
405
422
  value: "read-only",
406
423
  name: "只读模式",
424
+ nameEn: "Read Only",
407
425
  description: "禁止所有文件修改(write/edit/edit_soft)及任何非只读操作",
426
+ descriptionEn: "Forbids all file modification (write/edit/edit_soft) and any non-read-only operation",
408
427
  },
409
428
  {
410
429
  value: "workspace-write-never",
411
430
  name: "工作区内修改",
431
+ nameEn: "Workspace Write",
412
432
  description: "仅允许在当前工作区目录下修改文件,工作区外写操作一律拒绝",
433
+ descriptionEn: "Only files under the current workspace may be modified; writes outside it are always denied",
413
434
  },
414
435
  {
415
436
  value: "danger-full-access",
416
437
  name: "完全权限",
438
+ nameEn: "Full access",
417
439
  description: "允许修改任意目录文件及执行全量操作(需要二次确认)",
440
+ descriptionEn: "Allows modifying files anywhere and running any operation (needs a second confirmation)",
418
441
  },
419
442
  ];
443
+ /**
444
+ * 预设/权限文案随界面语言落定:中文界面用服务端默认文案,其它语言用
445
+ * nameEn ?? name(与审批规则的 labelEn/reasonEn、插件的 label/labelEn 同一约定;
446
+ * 其它语言包同理回落英文)。服务端 notice 的 textEn 也走这里,否则英文界面会看到
447
+ * `Switched to preset "全功能"`。
448
+ *
449
+ * lang 是 ServerLang(resolveServerLang 的产物):只有 "zh" 算中文。
450
+ * 本文件保持零依赖,故直接比字符串而不 import server/i18n.js。
451
+ */
452
+ export function localizedName(item, lang, fallback = "") {
453
+ if (!item)
454
+ return fallback;
455
+ return lang === "zh" ? (item.name ?? item.nameEn ?? fallback) : (item.nameEn ?? item.name ?? fallback);
456
+ }
457
+ /** localizedName 的描述版本(description 两边都可缺省 → undefined)。 */
458
+ export function localizedDescription(item, lang) {
459
+ if (!item)
460
+ return undefined;
461
+ return lang === "zh" ? (item.description ?? item.descriptionEn) : (item.descriptionEn ?? item.description);
462
+ }
420
463
  /** 按预设过滤活跃工具名。 */
421
464
  export function filterToolsByPreset(tools, preset) {
422
465
  const all = Array.from(tools);
@@ -437,6 +480,7 @@ export function filterToolsByPreset(tools, preset) {
437
480
  "edit",
438
481
  "edit_soft",
439
482
  "bash",
483
+ "powershell",
440
484
  "terminal_create",
441
485
  "terminal_input",
442
486
  "terminal_close",
@@ -466,13 +466,21 @@ function readVendoredPiCore(agentDir) {
466
466
  return null;
467
467
  }
468
468
  }
469
+ /**
470
+ * 宿主 SDK 及其 peer/内部包(@earendil-works/* / @mariozechner/*):
471
+ * 属于宿主引擎随核心一同分发或由别名提供的 SDK 内部构件,非用户安装的独立扩展组件;
472
+ * 既不应在组件列表重复展示,也不能被 `pi update npm:<name>` 更新(会报 No matching package found,issue #393)。
473
+ */
474
+ export function isHostProvidedPackage(name) {
475
+ return name === PI_CORE_PACKAGE || name.startsWith("@earendil-works/") || name.startsWith("@mariozechner/");
476
+ }
469
477
  /**
470
478
  * Build the full local target list: webui + the pi core + installed packages
471
479
  * + git-source extensions (issue #178) from global and project settings.
472
480
  * The pi core version comes from the CLI probe (injectable for tests), falling
473
481
  * back to the vendored copy under <agentDir>/npm/node_modules. Packages
474
- * listing the core directly are filtered out so the pi-core row wins — never
475
- * two rows for the same package.
482
+ * listing the core directly or host-provided SDK peers are filtered out so the
483
+ * pi-core row wins and host peers are not listed as updatable (issue #393).
476
484
  */
477
485
  export function collectTargets(agentDir, webuiVersion, probePiCore = defaultProbePiCore, opts) {
478
486
  const targets = [{ name: "pi-web-ui", version: webuiVersion, kind: "webui" }];
@@ -484,7 +492,7 @@ export function collectTargets(agentDir, webuiVersion, probePiCore = defaultProb
484
492
  kind: "pi-core",
485
493
  });
486
494
  }
487
- targets.push(...listInstalledPackages(agentDir).filter((pkg) => pkg.name !== PI_CORE_PACKAGE));
495
+ targets.push(...listInstalledPackages(agentDir).filter((pkg) => !isHostProvidedPackage(pkg.name)));
488
496
  targets.push(...listGitExtensions(agentDir, opts?.projectCwd));
489
497
  return targets;
490
498
  }
@@ -239,12 +239,16 @@ export function hasPendingWaitSubscription(options) {
239
239
  /**
240
240
  * 纯函数版置换决策:true = 保留(不得 dispose),false = 调用方可释放。
241
241
  * Pure decision core of displaceActive(): true = retain, false = may dispose.
242
- * 顺序与 displaceActive 保持一致:review/wizard → streaming → terminals →
242
+ * 顺序与 displaceActive 保持一致:pin → review/wizard → streaming → terminals →
243
243
  * active subagent run → pending wake → listed+continued(「打开后继续过」的会话也保留)。
244
244
  */
245
245
  export function shouldRetainActive(input) {
246
+ if (input.pinned)
247
+ return true;
246
248
  if (input.reviewing || input.wizardRunning)
247
249
  return true;
250
+ if (input.promptInFlight)
251
+ return true;
248
252
  if (input.streaming || input.compacting)
249
253
  return true;
250
254
  if (input.openTerminals > 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-ui",
3
- "version": "0.96.0",
3
+ "version": "0.97.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": {
@@ -83,8 +83,8 @@
83
83
  "lint:fix": "oxlint --fix server/ web/src/ extensions/ scripts/ desktop/"
84
84
  },
85
85
  "dependencies": {
86
- "@deepseek-ai/dsh-sdk-jsonrpc-server": "^0.1.1-rc.2",
87
- "@deepseek-ai/dsh-sdk-protocol": "^0.1.1-rc.2",
86
+ "@deepseek-ai/dsh-sdk-jsonrpc-server": "^0.1.5-rc.3",
87
+ "@deepseek-ai/dsh-sdk-protocol": "^0.1.5-rc.3",
88
88
  "@earendil-works/pi-coding-agent": ">=0.85.1",
89
89
  "@xterm/addon-fit": "^0.11.0",
90
90
  "@xterm/xterm": "^6.0.0",
@@ -149,11 +149,13 @@ body,
149
149
  letter-spacing: 0.06em;
150
150
  }
151
151
 
152
- /* 顶栏控件全部素色化:无底无框,hover 才现 hairline */
153
- .topbar .chip,
154
- .topbar .tb-tab,
155
- .topbar .panel-toggle,
156
- .topbar .plugin-topbar-item {
152
+ /* 顶栏控件全部素色化:无底无框,hover 才现 hairline。兜底所有 button /
153
+ * [role=button]——不管类名是谁(基线 chip / 插件注入 / capsule 等),顶栏里
154
+ * 一律裸图标,dropdown 触发器不许再带盒子。 */
155
+ .topbar button,
156
+ .topbar [role="button"],
157
+ .topbar a.chip,
158
+ .topbar .chip {
157
159
  background: transparent !important;
158
160
  border: 1px solid transparent !important;
159
161
  border-radius: 3px !important;
@@ -163,14 +165,20 @@ body,
163
165
  color 0.15s ease !important;
164
166
  }
165
167
 
166
- .topbar .chip:hover,
167
- .topbar .tb-tab:hover,
168
- .topbar .panel-toggle:hover,
169
- .topbar .plugin-topbar-item:hover {
168
+ .topbar button:hover,
169
+ .topbar [role="button"]:hover,
170
+ .topbar a.chip:hover,
171
+ .topbar .chip:hover {
170
172
  border-color: var(--border) !important;
171
173
  background: transparent !important;
172
174
  }
173
175
 
176
+ .topbar .dropdown {
177
+ background: transparent !important;
178
+ border: none !important;
179
+ box-shadow: none !important;
180
+ }
181
+
174
182
  /* ---- 2. 消息区:左缘墨线活动脊 + 文档化排版 ----
175
183
  * 列缘这条线让给脊线与输入框边框:脊线(虚线针脚)与输入框左边框同一条
176
184
  * 像素线,历史虚下来、接到输入框变实;消息正文整体右移 23px 挂在轨上。
@@ -190,35 +198,9 @@ body,
190
198
  padding: 2px 0 10px;
191
199
  }
192
200
 
193
- /* 操作按钮:行内簇已提升为基线默认(.chead-actions,随消息悬停显隐、
194
- 与行内复制按钮并排),主题里不再需要任何覆盖。 */
195
-
196
- /* 兜底路径(用户消息 / 纯文本回复,无 head 可宿主):底部操作行改为悬停
197
- 浮牌,钉在消息右上角的空纸面上 */
198
- .msg-actions {
199
- position: absolute;
200
- top: 0;
201
- right: 0;
202
- margin: 0;
203
- z-index: 2;
204
- padding: 2px;
205
- background: var(--menu-bg) !important;
206
- border: 1px solid var(--border) !important;
207
- border-radius: 4px !important;
208
- box-shadow: 0 4px 14px -4px rgba(0, 0, 0, 0.5) !important;
209
- opacity: 0;
210
- pointer-events: none;
211
- transition:
212
- opacity 0.12s ease,
213
- pointer-events 0.12s ease;
214
- }
215
-
216
- .msg:hover .msg-actions,
217
- .msg-actions.has-open-dropdown,
218
- .msg-actions:focus-within {
219
- opacity: 1;
220
- pointer-events: auto;
221
- }
201
+ /* 操作按钮:卡头只有自己的复制键(.chead-copy),消息级按钮(复制三件套 /
202
+ 分叉 / 回滚 / 朗读)统一在底部 .msg-actions 行,流内排在消息气泡/正文下边,
203
+ 桌面端与手机端保持一致。 */
222
204
 
223
205
  /* 角色行:书目式小标 */
224
206
  .msg-meta {
@@ -237,7 +219,8 @@ body,
237
219
  margin-left: -6px;
238
220
  }
239
221
 
240
- /* 正文块上的单一复制键同样去重:消息复制已在 head 行的操作簇里 */
222
+ /* 正文块上的单一复制键去重:卡头现在只有自己的复制键,消息级复制
223
+ (纯文本 / Markdown / 长图三件套)统一在底部 .msg-actions 行里。 */
241
224
  .msg-text-copy {
242
225
  display: none !important;
243
226
  }
@@ -469,6 +452,12 @@ div.chead[role="button"]:hover,
469
452
  border-bottom: 1px dotted var(--border-soft) !important;
470
453
  border-radius: 0 !important;
471
454
  box-shadow: none !important;
455
+ padding-left: 0 !important;
456
+ padding-right: 0 !important;
457
+ }
458
+
459
+ .msg-collapsed-action::before {
460
+ margin-left: 0 !important;
472
461
  }
473
462
 
474
463
  .msg-error {
@@ -487,7 +476,7 @@ div.chead[role="button"]:hover,
487
476
 
488
477
  .inputbox {
489
478
  background: var(--inputbox-bg) !important;
490
- border: 1px solid var(--border) !important;
479
+ border: none !important;
491
480
  border-radius: 3px !important;
492
481
  box-shadow: inset 3px 0 0 0 rgba(224, 90, 53, 0.4) !important;
493
482
  transition:
@@ -496,14 +485,14 @@ div.chead[role="button"]:hover,
496
485
  }
497
486
 
498
487
  .inputbox:focus-within {
499
- border-color: var(--accent) !important;
488
+ border: none !important;
500
489
  box-shadow: inset 3px 0 0 0 var(--accent) !important;
501
490
  }
502
491
 
503
492
  .btn.send {
504
493
  background: var(--send-blue) !important;
505
494
  color: #131313 !important;
506
- border-radius: 3px !important;
495
+ border-radius: 50% !important;
507
496
  box-shadow: none !important;
508
497
  font-weight: 600;
509
498
  }
@@ -515,7 +504,54 @@ div.chead[role="button"]:hover,
515
504
  .btn.stop {
516
505
  background: var(--stop-red) !important;
517
506
  color: #131313 !important;
507
+ border-radius: 50% !important;
508
+ }
509
+
510
+ /* 输入框底部模型选择、思考强度、预设等按钮:去边框、透明底,hover 现 hairline */
511
+ .composer-tools {
512
+ border-top: none !important;
513
+ }
514
+
515
+ .composer-tools .chip,
516
+ .composer-tools .capsule-segment {
517
+ font-size: 13px !important;
518
+ padding: 4px 10px !important;
519
+ gap: 6px !important;
520
+ background: transparent !important;
521
+ border: 1px solid transparent !important;
522
+ box-shadow: none !important;
518
523
  border-radius: 3px !important;
524
+ transition:
525
+ border-color 0.15s ease,
526
+ background 0.15s ease !important;
527
+ }
528
+
529
+ .composer-tools .model-thinking-capsule {
530
+ background: transparent !important;
531
+ border: none !important;
532
+ box-shadow: none !important;
533
+ padding: 0 !important;
534
+ }
535
+
536
+ .composer-tools .model-thinking-capsule .capsule-divider {
537
+ display: none !important;
538
+ }
539
+
540
+ .composer-tools .chip:hover,
541
+ .composer-tools .capsule-segment:hover {
542
+ border-color: var(--border) !important;
543
+ background: transparent !important;
544
+ }
545
+
546
+ .composer-tools-left {
547
+ gap: 6px !important;
548
+ }
549
+
550
+ .composer-tools .chip-model,
551
+ .composer-tools .chip-sub,
552
+ .composer-tools .capsule-model-name,
553
+ .composer-tools .capsule-thinking-text {
554
+ font-size: 12.5px !important;
519
555
  }
520
556
 
521
557
  /* ---- 9. 底栏 / 状态栏:hairline 收边 ---- */
@@ -606,8 +642,7 @@ div.chead[role="button"]:hover,
606
642
  0 16px 40px -12px rgba(0, 0, 0, 0.55) !important;
607
643
  }
608
644
 
609
- .dd-menu,
610
- .dropdown {
645
+ .dd-menu {
611
646
  background: var(--menu-bg) !important;
612
647
  border: 1px solid var(--border) !important;
613
648
  border-radius: 4px !important;
package/themes/zhupi.css CHANGED
@@ -154,11 +154,13 @@ body,
154
154
  letter-spacing: 0.06em;
155
155
  }
156
156
 
157
- /* 顶栏控件全部素色化:无底无框,hover 才现 hairline */
158
- .topbar .chip,
159
- .topbar .tb-tab,
160
- .topbar .panel-toggle,
161
- .topbar .plugin-topbar-item {
157
+ /* 顶栏控件全部素色化:无底无框,hover 才现 hairline。兜底所有 button /
158
+ * [role=button]——不管类名是谁(基线 chip / 插件注入 / capsule 等),顶栏里
159
+ * 一律裸图标,dropdown 触发器不许再带盒子。 */
160
+ .topbar button,
161
+ .topbar [role="button"],
162
+ .topbar a.chip,
163
+ .topbar .chip {
162
164
  background: transparent !important;
163
165
  border: 1px solid transparent !important;
164
166
  border-radius: 3px !important;
@@ -168,14 +170,20 @@ body,
168
170
  color 0.15s ease !important;
169
171
  }
170
172
 
171
- .topbar .chip:hover,
172
- .topbar .tb-tab:hover,
173
- .topbar .panel-toggle:hover,
174
- .topbar .plugin-topbar-item:hover {
173
+ .topbar button:hover,
174
+ .topbar [role="button"]:hover,
175
+ .topbar a.chip:hover,
176
+ .topbar .chip:hover {
175
177
  border-color: var(--border) !important;
176
178
  background: transparent !important;
177
179
  }
178
180
 
181
+ .topbar .dropdown {
182
+ background: transparent !important;
183
+ border: none !important;
184
+ box-shadow: none !important;
185
+ }
186
+
179
187
  /* ---- 2. 消息区:左缘墨线活动脊 + 文档化排版 ----
180
188
  * 列缘这条线让给脊线与输入框边框:脊线(虚线针脚)与输入框左边框同一条
181
189
  * 像素线,历史虚下来、接到输入框变实,像「线活了起来」;消息正文整体
@@ -197,35 +205,9 @@ body,
197
205
  padding: 2px 0 10px;
198
206
  }
199
207
 
200
- /* 操作按钮:行内簇已提升为基线默认(.chead-actions,随消息悬停显隐、
201
- 与行内复制按钮并排),主题里不再需要任何覆盖。 */
202
-
203
- /* 兜底路径(用户消息 / 纯文本回复,无 head 可宿主):底部操作行改为悬停
204
- 浮牌,钉在消息右上角的空纸面上 */
205
- .msg-actions {
206
- position: absolute;
207
- top: 0;
208
- right: 0;
209
- margin: 0;
210
- z-index: 2;
211
- padding: 2px;
212
- background: var(--menu-bg) !important;
213
- border: 1px solid var(--border) !important;
214
- border-radius: 4px !important;
215
- box-shadow: 0 4px 14px -4px rgba(38, 34, 24, 0.35) !important;
216
- opacity: 0;
217
- pointer-events: none;
218
- transition:
219
- opacity 0.12s ease,
220
- pointer-events 0.12s ease;
221
- }
222
-
223
- .msg:hover .msg-actions,
224
- .msg-actions.has-open-dropdown,
225
- .msg-actions:focus-within {
226
- opacity: 1;
227
- pointer-events: auto;
228
- }
208
+ /* 操作按钮:卡头只有自己的复制键(.chead-copy),消息级按钮(复制三件套 /
209
+ 分叉 / 回滚 / 朗读)统一在底部 .msg-actions 行,流内排在消息气泡/正文下边,
210
+ 桌面端与手机端保持一致。 */
229
211
 
230
212
  /* 角色行:书目式小标 */
231
213
  .msg-meta {
@@ -244,7 +226,8 @@ body,
244
226
  margin-left: -6px;
245
227
  }
246
228
 
247
- /* 正文块上的单一复制键同样去重:消息复制已在 head 行的操作簇里 */
229
+ /* 正文块上的单一复制键去重:卡头现在只有自己的复制键,消息级复制
230
+ (纯文本 / Markdown / 长图三件套)统一在底部 .msg-actions 行里。 */
248
231
  .msg-text-copy {
249
232
  display: none !important;
250
233
  }
@@ -481,6 +464,12 @@ div.chead[role="button"]:hover,
481
464
  border-bottom: 1px dotted var(--border-soft) !important;
482
465
  border-radius: 0 !important;
483
466
  box-shadow: none !important;
467
+ padding-left: 0 !important;
468
+ padding-right: 0 !important;
469
+ }
470
+
471
+ .msg-collapsed-action::before {
472
+ margin-left: 0 !important;
484
473
  }
485
474
 
486
475
  .msg-error {
@@ -499,7 +488,7 @@ div.chead[role="button"]:hover,
499
488
 
500
489
  .inputbox {
501
490
  background: var(--inputbox-bg) !important;
502
- border: 1px solid var(--border) !important;
491
+ border: none !important;
503
492
  border-radius: 3px !important;
504
493
  box-shadow: inset 3px 0 0 0 rgba(191, 59, 31, 0.35) !important;
505
494
  transition:
@@ -508,14 +497,14 @@ div.chead[role="button"]:hover,
508
497
  }
509
498
 
510
499
  .inputbox:focus-within {
511
- border-color: var(--accent) !important;
500
+ border: none !important;
512
501
  box-shadow: inset 3px 0 0 0 var(--accent) !important;
513
502
  }
514
503
 
515
504
  .btn.send {
516
505
  background: var(--send-blue) !important;
517
506
  color: #f6f2e9 !important;
518
- border-radius: 3px !important;
507
+ border-radius: 50% !important;
519
508
  box-shadow: none !important;
520
509
  font-weight: 600;
521
510
  }
@@ -527,7 +516,54 @@ div.chead[role="button"]:hover,
527
516
  .btn.stop {
528
517
  background: var(--stop-red) !important;
529
518
  color: #f6f2e9 !important;
519
+ border-radius: 50% !important;
520
+ }
521
+
522
+ /* 输入框底部模型选择、思考强度、预设等按钮:去边框、透明底,hover 现 hairline */
523
+ .composer-tools {
524
+ border-top: none !important;
525
+ }
526
+
527
+ .composer-tools .chip,
528
+ .composer-tools .capsule-segment {
529
+ font-size: 13px !important;
530
+ padding: 4px 10px !important;
531
+ gap: 6px !important;
532
+ background: transparent !important;
533
+ border: 1px solid transparent !important;
534
+ box-shadow: none !important;
530
535
  border-radius: 3px !important;
536
+ transition:
537
+ border-color 0.15s ease,
538
+ background 0.15s ease !important;
539
+ }
540
+
541
+ .composer-tools .model-thinking-capsule {
542
+ background: transparent !important;
543
+ border: none !important;
544
+ box-shadow: none !important;
545
+ padding: 0 !important;
546
+ }
547
+
548
+ .composer-tools .model-thinking-capsule .capsule-divider {
549
+ display: none !important;
550
+ }
551
+
552
+ .composer-tools .chip:hover,
553
+ .composer-tools .capsule-segment:hover {
554
+ border-color: var(--border) !important;
555
+ background: transparent !important;
556
+ }
557
+
558
+ .composer-tools-left {
559
+ gap: 6px !important;
560
+ }
561
+
562
+ .composer-tools .chip-model,
563
+ .composer-tools .chip-sub,
564
+ .composer-tools .capsule-model-name,
565
+ .composer-tools .capsule-thinking-text {
566
+ font-size: 12.5px !important;
531
567
  }
532
568
 
533
569
  /* ---- 9. 底栏 / 状态栏:hairline 收边 ---- */
@@ -659,8 +695,7 @@ div.chead[role="button"]:hover,
659
695
  0 16px 40px -12px rgba(38, 34, 24, 0.25) !important;
660
696
  }
661
697
 
662
- .dd-menu,
663
- .dropdown {
698
+ .dd-menu {
664
699
  background: var(--menu-bg) !important;
665
700
  border: 1px solid var(--border) !important;
666
701
  border-radius: 4px !important;
@@ -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-Do9RgJC3.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-oI7UKOqJ.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};