pi-web-ui 0.64.5 → 0.64.6

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 (77) hide show
  1. package/README.md +2 -2
  2. package/dist/server/agent-service.js +95 -5
  3. package/dist/server/bg-servers.js +111 -26
  4. package/dist/server/client-state.js +2 -0
  5. package/dist/server/dsh/dsh-agent-service.js +2 -0
  6. package/dist/server/index.js +1 -0
  7. package/dist/server/process-utils.js +41 -0
  8. package/dist/server/protocol-version.js +1 -1
  9. package/dist/server/settings-service.js +33 -0
  10. package/dist/server/subagent-templates.js +14 -1
  11. package/dist/server/subagents.js +107 -11
  12. package/package.json +110 -110
  13. package/web/dist/assets/{TerminalPanel-7xM8bK8t.js → TerminalPanel-CoPFNu1d.js} +1 -1
  14. package/web/dist/assets/{abnfDiagram-VCTEODGH-dzrhTpxN.js → abnfDiagram-VCTEODGH-Dltz1N0G.js} +1 -1
  15. package/web/dist/assets/{arc-CvEtLzuM.js → arc-QOm2xRll.js} +1 -1
  16. package/web/dist/assets/{architectureDiagram-5GKGNRK7-1d4TEc2z.js → architectureDiagram-5GKGNRK7-L3XUBFbb.js} +1 -1
  17. package/web/dist/assets/{blockDiagram-I7D4REHJ-B8HBpC0d.js → blockDiagram-I7D4REHJ-CJmza4yx.js} +1 -1
  18. package/web/dist/assets/{c4Diagram-7LVT6UL2-CNGfPB3f.js → c4Diagram-7LVT6UL2-CwgA5WEW.js} +1 -1
  19. package/web/dist/assets/channel-VncP7yyT.js +1 -0
  20. package/web/dist/assets/{chunk-2Q5K7J3B-CSVK1bvg.js → chunk-2Q5K7J3B-DoqSiCVF.js} +1 -1
  21. package/web/dist/assets/{chunk-5VM5RSS4-BxIozqQs.js → chunk-5VM5RSS4-CO14GhQ1.js} +1 -1
  22. package/web/dist/assets/{chunk-F27PBJKO-QnkX6p93.js → chunk-F27PBJKO-kRFf9SDx.js} +1 -1
  23. package/web/dist/assets/{chunk-IMKFNOWR-kBTZpeLx.js → chunk-IMKFNOWR-Cm6JOqm0.js} +1 -1
  24. package/web/dist/assets/{chunk-JWPE2WC7-CcF10PqG.js → chunk-JWPE2WC7-DTpnGTdB.js} +1 -1
  25. package/web/dist/assets/{chunk-POPQ4Y6H-Ch9HWJm9.js → chunk-POPQ4Y6H-CdSt_7xq.js} +1 -1
  26. package/web/dist/assets/{chunk-SVP7TREG-D-6N0S8z.js → chunk-SVP7TREG-IQRTD_4U.js} +1 -1
  27. package/web/dist/assets/{chunk-TICWLB2K-CSDXk9NS.js → chunk-TICWLB2K-Duj1rt_5.js} +1 -1
  28. package/web/dist/assets/{chunk-XXDRQBXY-C7tgRoeO.js → chunk-XXDRQBXY-BFf3gcbL.js} +1 -1
  29. package/web/dist/assets/classDiagram-ZZMXUADV-BWCNDr-F.js +1 -0
  30. package/web/dist/assets/classDiagram-v2-VYDZK3BY-BWCNDr-F.js +1 -0
  31. package/web/dist/assets/{cose-bilkent-JH36ORCC-V45ICdWW.js → cose-bilkent-JH36ORCC-D5k0Z1dx.js} +1 -1
  32. package/web/dist/assets/{cynefin-OW5HDTMX-DkjrprMv.js → cynefin-OW5HDTMX-OsV-x0h-.js} +1 -1
  33. package/web/dist/assets/{cynefinDiagram-5FMLGOSQ-CSFzGNbH.js → cynefinDiagram-5FMLGOSQ-DM7XHm4s.js} +1 -1
  34. package/web/dist/assets/{dagre-GXQ25YYZ-B2tQ52ot.js → dagre-GXQ25YYZ-B0bIbgMh.js} +1 -1
  35. package/web/dist/assets/{diagram-S7CK7UJ4-CAeTly-S.js → diagram-S7CK7UJ4-lMVsMjfI.js} +1 -1
  36. package/web/dist/assets/{diagram-UQ7AKVKN-D6_khPwH.js → diagram-UQ7AKVKN-BJDbkaYf.js} +1 -1
  37. package/web/dist/assets/{diagram-VSXAHHWV-DDaGr2Z2.js → diagram-VSXAHHWV-1zUfVr-L.js} +1 -1
  38. package/web/dist/assets/{diagram-VX7I27RA-8nsSxNoG.js → diagram-VX7I27RA-B-Qy3dth.js} +1 -1
  39. package/web/dist/assets/{diagram-Z3DM3KII-CQFF2G65.js → diagram-Z3DM3KII-DbKcoZFP.js} +1 -1
  40. package/web/dist/assets/{ebnfDiagram-PWID7BFC-CmR6-1Mj.js → ebnfDiagram-PWID7BFC-CuMZBzDi.js} +1 -1
  41. package/web/dist/assets/{erDiagram-RLTQ6QDP-CCnQ4eXZ.js → erDiagram-RLTQ6QDP-Dkw_Uhg2.js} +1 -1
  42. package/web/dist/assets/{flowDiagram-HODETNUW-bpunOdNf.js → flowDiagram-HODETNUW-CwDYMy38.js} +1 -1
  43. package/web/dist/assets/{ganttDiagram-EL5Y4UJY-Do5WVm61.js → ganttDiagram-EL5Y4UJY-B2s3myqg.js} +1 -1
  44. package/web/dist/assets/{gitGraphDiagram-WWUBYQGX-DRBgLSBf.js → gitGraphDiagram-WWUBYQGX-CJ3rUTG5.js} +1 -1
  45. package/web/dist/assets/{index-D2iRpLNi.js → index-30RM921r.js} +19 -19
  46. package/web/dist/assets/index-B_lYcsGw.css +10 -0
  47. package/web/dist/assets/{infoDiagram-27XIBGKW-9xdIyGW9.js → infoDiagram-27XIBGKW-DuIoOHyL.js} +1 -1
  48. package/web/dist/assets/{ishikawaDiagram-5VMMS53U-C1HHSxnn.js → ishikawaDiagram-5VMMS53U-Fm14e_CH.js} +1 -1
  49. package/web/dist/assets/{journeyDiagram-3NMN7TZE-BTroj2Jo.js → journeyDiagram-3NMN7TZE-BuokOq_3.js} +1 -1
  50. package/web/dist/assets/{kanban-definition-UXKFOSKX-ByvuSwPg.js → kanban-definition-UXKFOSKX-D2FQMRnh.js} +1 -1
  51. package/web/dist/assets/{layout-CJG7qpZa.js → layout-CZUvkkNF.js} +1 -1
  52. package/web/dist/assets/{linear-CIQgXZYV.js → linear-tIFeS1QT.js} +1 -1
  53. package/web/dist/assets/{mermaid.core-CkrYNntI.js → mermaid.core-BI5_3AOv.js} +5 -5
  54. package/web/dist/assets/{mindmap-definition-YA3MSWOX-C2wu2sJd.js → mindmap-definition-YA3MSWOX-D5RmpgpB.js} +1 -1
  55. package/web/dist/assets/{pegDiagram-XKGWAZYB-pyyiTfDO.js → pegDiagram-XKGWAZYB-DpZxaRI3.js} +1 -1
  56. package/web/dist/assets/{pieDiagram-E7YTZNPT-BwJv35J9.js → pieDiagram-E7YTZNPT-Co7MADQz.js} +1 -1
  57. package/web/dist/assets/{quadrantDiagram-AXDQQJYC-1Q0wSsql.js → quadrantDiagram-AXDQQJYC-BsRraVQ-.js} +1 -1
  58. package/web/dist/assets/{railroadDiagram-O6MQD6OU-DWFZGo9j.js → railroadDiagram-O6MQD6OU-DW2pw5p_.js} +1 -1
  59. package/web/dist/assets/{requirementDiagram-BXWQKSXE-CNgNvQO7.js → requirementDiagram-BXWQKSXE-DeaEY9Dk.js} +1 -1
  60. package/web/dist/assets/{sankeyDiagram-P5KCCOFB-DWmOe-31.js → sankeyDiagram-P5KCCOFB-BlKg1xhH.js} +1 -1
  61. package/web/dist/assets/{sequenceDiagram-WJ2MYXX4-CvFRnceu.js → sequenceDiagram-WJ2MYXX4-C0ffpRmx.js} +1 -1
  62. package/web/dist/assets/{sizeCapture-INFHLROL-DNyzv4qr.js → sizeCapture-INFHLROL-DHFDdkAo.js} +1 -1
  63. package/web/dist/assets/{stateDiagram-D77RDMKH-CUOwyJAj.js → stateDiagram-D77RDMKH-CwKStGSS.js} +1 -1
  64. package/web/dist/assets/stateDiagram-v2-MP3YSRHH-B0qJoPpx.js +1 -0
  65. package/web/dist/assets/{swimlanes-42K2YHIH-B2tqXq5C.js → swimlanes-42K2YHIH-MB6Uskqk.js} +1 -1
  66. package/web/dist/assets/swimlanesDiagram-VR7AAH4N-IYPIFoU6.js +8 -0
  67. package/web/dist/assets/{timeline-definition-24CTP7MA-BC349pHg.js → timeline-definition-24CTP7MA-CkcuHQ1A.js} +1 -1
  68. package/web/dist/assets/{vennDiagram-4TSXK5OY-Dp_8OJ1R.js → vennDiagram-4TSXK5OY-DRiGv5ga.js} +1 -1
  69. package/web/dist/assets/{wardleyDiagram-VM6X3IG4-aZq6zGi4.js → wardleyDiagram-VM6X3IG4-Cwldlf89.js} +1 -1
  70. package/web/dist/assets/{xychartDiagram-S5SC5T6Z-Cyq-4VPK.js → xychartDiagram-S5SC5T6Z-Biqu8RpV.js} +1 -1
  71. package/web/dist/index.html +2 -2
  72. package/web/dist/assets/channel-DXmrQIC_.js +0 -1
  73. package/web/dist/assets/classDiagram-ZZMXUADV-sfceoHNh.js +0 -1
  74. package/web/dist/assets/classDiagram-v2-VYDZK3BY-sfceoHNh.js +0 -1
  75. package/web/dist/assets/index-BVwF4g4j.css +0 -10
  76. package/web/dist/assets/stateDiagram-v2-MP3YSRHH-CqyXAKwd.js +0 -1
  77. package/web/dist/assets/swimlanesDiagram-VR7AAH4N-DzTxUWmy.js +0 -8
package/README.md CHANGED
@@ -70,8 +70,8 @@ theme switching, and a full settings panel — tuned for daily development.
70
70
 
71
71
  ### 🤖 Subagents & templates
72
72
 
73
- - **First-party subagents** — spawn independent background conversations for parallel exploration / implementation / review (`subagent_spawn`); manage them like a chat right in the left panel: view live output, inject follow-ups (steer), abort, dismiss. In-memory sessions — they never touch the history / resume list, and can be nested.
74
- - **Subagent templates** — configure reusable presets in Settings → Subagent templates: a role system prompt (append or replace) plus skills & extensions whitelists. The AI picks one via the `subagent_templates` tool and `subagent_spawn(template="…")`, or spawns without one (main-session default config). Disabled templates stay in the panel for re-enabling but become invisible to the AI tools (can't be listed or picked). Templates are shared globally across browser clients (`<dataDir>/subagent-templates.json`). Six built-in templates (review / implement / research / scout / audit / delegate, adapted from the pi-subagents community projects) seed the list on first run — marked 「Built-in」, editable and deletable like any other.
73
+ - **First-party subagents** — spawn independent background conversations for parallel exploration / implementation / review (`subagent_spawn`, with optional `model` override or a template's model); collect results without polling via `subagent_wait_all` (blocks until every subagent finishes, then summarizes results/errors). Manage them like a chat right in the left panel: view live output, inject follow-ups (steer), abort, dismiss — failed runs surface a red dot in the running list and an error notice in the main chat. In-memory sessions — they never touch the history / resume list, and can be nested.
74
+ - **Subagent templates** — configure reusable presets in Settings → Subagent templates: a role system prompt (append or replace), skills & extensions whitelists, and an optional per-template model. The AI picks one via the `subagent_templates` tool and `subagent_spawn(template="…")`, or spawns without one (default = follow the main conversation's current model, or the global default subagent model set in the same panel). Disabled templates stay in the panel for re-enabling but become invisible to the AI tools (can't be listed or picked). Templates are shared globally across browser clients (`<dataDir>/subagent-templates.json`). Six built-in templates (review / implement / research / scout / audit / delegate, adapted from the pi-subagents community projects) seed the list on first run — marked 「Built-in」, editable and deletable like any other.
75
75
 
76
76
  ### 🖼️ Files, images & attachments
77
77
 
@@ -589,8 +589,12 @@ export class ClientSession {
589
589
  }
590
590
  }
591
591
  /** 创建子代理 conversation(inMemory runtime + 独立 terminals),listed 入左栏,
592
- * 并在其上触发一次完整回合。返回 convId(= 工具 runId)。 */
593
- async spawnSubagentConversation(prompt, type, cwd, apply) {
592
+ * 并在其上触发一次完整回合。返回 convId(= 工具 runId)。
593
+ *
594
+ * `model`(可选):"provider/id",显式指定子代理模型。不传时由调用方决定是否
595
+ * 回退到模板模型 / 设置面板默认模型;null = 跟随主对话当前模型(默认行为,
596
+ * runtime 重建时会继承共享 ModelRuntime 的当前默认)。 */
597
+ async spawnSubagentConversation(prompt, type, cwd, apply, model) {
594
598
  const conversationId = `sa-${randomUUID().slice(0, 8)}`;
595
599
  const terminals = this.makeTerminalManager(conversationId, cwd);
596
600
  const runtime = await createAgentSessionRuntime(this.makeRuntimeFactory(terminals, apply), {
@@ -614,6 +618,41 @@ export class ClientSession {
614
618
  catch {
615
619
  // 绑定失败不阻断运行。
616
620
  }
621
+ // 指定模型(显式 model 参数 → 模板 model → 设置面板默认)时,在首回合前
622
+ // 给子代理会话换模型;全都不给 = 跟随主对话:把发起会话当前的模型也
623
+ // 显式搬过来(新 runtime 的默认模型未必等于主对话刚选的模型)。
624
+ const resolvedModel = model ?? (apply?.model?.trim() || null) ?? (this.settingsSvc.current.subagentDefaultModel || null);
625
+ const followModel = resolvedModel ? resolvedModel : this.session.model ? `${this.session.model.provider}/${this.session.model.id}` : null;
626
+ if (followModel) {
627
+ const slash = followModel.indexOf("/");
628
+ const m = slash > 0 && slash < followModel.length - 1
629
+ ? this.sharedModelRuntime?.getModel(followModel.slice(0, slash), followModel.slice(slash + 1))
630
+ : undefined;
631
+ if (m) {
632
+ try {
633
+ // 先恢复该 provider 的项目密钥(setModel 的鉴权检查要用),再换模型。
634
+ await this.restoreKeyForModel(followModel, cwd);
635
+ await conv.session.setModel(m);
636
+ }
637
+ catch (err) {
638
+ // 换模型失败不阻断运行——沿用默认模型继续。
639
+ this.emit({
640
+ type: "notice",
641
+ level: "warning",
642
+ text: `子代理模型切换失败(将按默认模型运行):${followModel}(${err.message})`,
643
+ textEn: `Failed to set subagent model, running with default: ${followModel} (${err.message})`,
644
+ });
645
+ }
646
+ }
647
+ else {
648
+ this.emit({
649
+ type: "notice",
650
+ level: "warning",
651
+ text: `子代理模型不存在,将按默认模型运行:${followModel}`,
652
+ textEn: `Subagent model not found, running with default: ${followModel}`,
653
+ });
654
+ }
655
+ }
617
656
  // 触发回合(后台执行;失败转识为通知)。
618
657
  void conv.session.sendUserMessage(prompt).catch((err) => {
619
658
  this.emit({
@@ -648,6 +687,7 @@ export class ClientSession {
648
687
  catch {
649
688
  // session being replaced — report defaults
650
689
  }
690
+ const { error, canceled } = this.subagentRunOutcome(conv);
651
691
  return {
652
692
  convId: conv.id,
653
693
  type: conv.subagentType ?? "general",
@@ -655,11 +695,38 @@ export class ClientSession {
655
695
  prompt: "",
656
696
  state,
657
697
  streaming,
698
+ error,
699
+ canceled,
658
700
  messageCount,
659
701
  model: conv.session.model?.id,
660
702
  output: conv.session.getLastAssistantText() ?? "",
661
703
  };
662
704
  }
705
+ /** 子代理最近一次运行的结局:最后一条 assistant 消息的 errorMessage / stopReason。
706
+ * 报错 > 中止 > 正常,三者互斥;无 assistant 消息时返回空。 */
707
+ subagentRunOutcome(conv) {
708
+ try {
709
+ const msgs = conv.session.agent.state.messages;
710
+ for (let i = msgs.length - 1; i >= 0; i--) {
711
+ const m = msgs[i];
712
+ if (m.role !== "assistant")
713
+ continue;
714
+ const err = m.errorMessage;
715
+ if (typeof err === "string" && err.trim()) {
716
+ return { error: err.trim() };
717
+ }
718
+ const stop = m.stopReason;
719
+ if (stop === "aborted" || stop === "cancelled") {
720
+ return { canceled: true };
721
+ }
722
+ break;
723
+ }
724
+ }
725
+ catch {
726
+ // session being replaced — treat as no outcome yet
727
+ }
728
+ return {};
729
+ }
663
730
  emitTerminal(conversationId, msg) {
664
731
  // Background conversations keep collecting output in their own PTY buffer.
665
732
  // Do not stream it into the active xterm; push the retained window on switch.
@@ -738,13 +805,14 @@ export class ClientSession {
738
805
  * 切换查看 / 输入补充(steer)/ 中止(abort)/ 移出全部复用现有对话机制。
739
806
  */
740
807
  subagentHost = {
741
- spawnSubagent: (prompt, type, cwd, templateName) => {
808
+ spawnSubagent: (prompt, type, cwd, templateName, model) => {
742
809
  // 模板:存在且启用时应用;传了名字但不可用 → 抛错让工具转给 AI。
743
810
  const tpl = templateName ? this.subagentTemplates.get(templateName) : undefined;
744
811
  if (templateName && (!tpl || !tpl.enabled)) {
745
812
  throw new Error(`子代理模板不可用:${templateName}(不存在或已停用)`);
746
813
  }
747
- return this.spawnSubagentConversation(prompt, type, cwd, tpl);
814
+ // 模型优先级:显式 model 参数 > 模板自带模型 > 设置面板默认模型;都不给 = 跟随主对话。
815
+ return this.spawnSubagentConversation(prompt, type, cwd, tpl, model);
748
816
  },
749
817
  getSubagent: (convId) => this.getSubagentSnapshot(convId),
750
818
  listSubagents: () => this.listSubagentSnapshots(),
@@ -764,7 +832,7 @@ export class ClientSession {
764
832
  listTemplates: () => this.subagentTemplates
765
833
  .list()
766
834
  .filter((t) => t.enabled)
767
- .map((t) => ({ name: t.name, description: t.description })),
835
+ .map((t) => ({ name: t.name, description: t.description, model: t.model })),
768
836
  isTemplateUsable: (name) => {
769
837
  const t = this.subagentTemplates.get(name);
770
838
  return !!t && t.enabled;
@@ -1456,6 +1524,26 @@ export class ClientSession {
1456
1524
  }
1457
1525
  break;
1458
1526
  }
1527
+ // 子代理运行报错(provider 400 / 超时等)→ 通知主对话,让用户/AI 知道
1528
+ // 拿回的结果可能是空或无意义的(否则子代理只是安静地停在「done」,
1529
+ // 主对话永远收不到失败信号)。错误文本变化时允许再次通知(去重)。
1530
+ if (conv.isSubagent) {
1531
+ const { error } = this.subagentRunOutcome(conv);
1532
+ if (error && error !== conv.subagentErrorNotified) {
1533
+ conv.subagentError = error;
1534
+ conv.subagentErrorNotified = error;
1535
+ this.emit({
1536
+ type: "notice",
1537
+ level: "error",
1538
+ text: `子代理 ${conv.id.slice(0, 8)}(${conv.subagentType ?? "general"})运行失败:${error}`,
1539
+ textEn: `Subagent ${conv.id.slice(0, 8)} (${conv.subagentType ?? "general"}) failed: ${error}`,
1540
+ });
1541
+ }
1542
+ else if (error) {
1543
+ conv.subagentError = error;
1544
+ }
1545
+ this.emitConversations();
1546
+ }
1459
1547
  // Goal review hook lives in GoalService.onAgentEnd(conv, false).
1460
1548
  this.goalSvc.onAgentEnd(conv, false);
1461
1549
  // Deferred settings reload: settings (system prompt / skills /
@@ -2907,6 +2995,8 @@ export class ClientSession {
2907
2995
  messageCount,
2908
2996
  isStreaming,
2909
2997
  isSubagent: !!conv.isSubagent,
2998
+ // 子代理带 error 标记:左栏红点提示(普通对话不参与)。
2999
+ ...(conv.isSubagent ? this.subagentRunOutcome(conv) : {}),
2910
3000
  });
2911
3001
  }
2912
3002
  this.emit({
@@ -1,7 +1,68 @@
1
- import { killPidTree, lookupProcessName, lookupProcessCommandLine, snapshotListeningPorts } from "./process-utils.js";
1
+ import { killPidTree, lookupProcessName, lookupProcessCommandLine, snapshotListeningPorts, snapshotProcessParents, } from "./process-utils.js";
2
2
  const BG_REFRESH_INTERVAL_MS = 30_000;
3
3
  /** bash 结束后等这么久再拍「后」快照——给后台服务绑定端口的时间。 */
4
4
  const BG_BIND_WAIT_MS = 1500;
5
+ /**
6
+ * 基本不可能由 AI 启动的常驻桌面软件进程名(小写)。命中即跳过,避免面板被
7
+ * 微信/QQ 等本地软件的动态监听端口污染。注意 Chrome 不进黑名单:AI 会用
8
+ * Playwright 拉浏览器做模拟,它靠下方父链回溯判定(Playwright 起的 chrome
9
+ * 能回溯到服务器进程,自己开的 chrome 父链是 explorer,分得开)。
10
+ */
11
+ export const NON_AGENT_PROCESS_NAMES = new Set([
12
+ "wechat.exe",
13
+ "weixin.exe",
14
+ "wechatappex.exe",
15
+ "qq.exe",
16
+ "tim.exe",
17
+ "telegram.exe",
18
+ "dingtalk.exe",
19
+ "explorer.exe",
20
+ "searchhost.exe",
21
+ "searchapp.exe",
22
+ "svchost.exe",
23
+ "winlogon.exe",
24
+ "dwm.exe",
25
+ "csrss.exe",
26
+ "conhost.exe",
27
+ ]);
28
+ /**
29
+ * 判定 bash 后新出现的监听端口进程是否该记入后台任务列表(即「AI 启动的」)。
30
+ * 判定规则(查不到就保守记录,宁多勿漏):
31
+ * 1. 进程名命中黑名单(如 WeChat.exe,AI 不会去启动微信)→ 跳过;
32
+ * 2. 进程树可查时沿父链向上回溯:
33
+ * - 撞上服务器进程(serverPid,AI 的任何 bash/execFile 都从它 spawn)→ 记录;
34
+ * - 撞上本次 diff 出的其他新 pid(bash 留下的中间层,如 concurrently/npm
35
+ * 一类进程,它自己不监听端口所以没进 diff,但子进程监听)→ 记录;
36
+ * - 完整回溯到系统根(pid 0/1)都未命中 → 桌面软件自启(如自己开的 Chrome,
37
+ * 父链 explorer→…→0),跳过;
38
+ * 3. 父链断链(父进程已退出/reparent,快照里查不到级联)→ 保守记录。
39
+ * @param parents 全量 pid→ppid 映射,undefined = 查询失败(跳过父链判定,只留黑名单)。
40
+ * @param newPids 本次 bash 前后 diff 出的全部新监听 pid。
41
+ */
42
+ export function shouldTrackBackgroundServer(pid, parents, newPids, serverPid, name) {
43
+ if (name && NON_AGENT_PROCESS_NAMES.has(name.toLowerCase()))
44
+ return false;
45
+ if (!parents)
46
+ return true; // 进程树查不到 → 保守记录
47
+ const seen = new Set();
48
+ let cur = pid;
49
+ while (cur !== undefined && !seen.has(cur)) {
50
+ seen.add(cur);
51
+ if (cur === serverPid)
52
+ return true;
53
+ if (cur !== pid && newPids.has(cur))
54
+ return true;
55
+ const next = parents.get(cur);
56
+ if (next === undefined) {
57
+ // 到达系统根(0/1)→ 完整链,桌面软件;否则是断链 → 保守记录
58
+ return cur <= 1 ? false : true;
59
+ }
60
+ if (next === cur)
61
+ return false; // 自引用根,同样视为完整链终点
62
+ cur = next;
63
+ }
64
+ return false;
65
+ }
5
66
  export class BgServerTracker {
6
67
  opts;
7
68
  servers = new Map();
@@ -38,38 +99,62 @@ export class BgServerTracker {
38
99
  return;
39
100
  await new Promise((r) => setTimeout(r, BG_BIND_WAIT_MS));
40
101
  const after = await snapshotListeningPorts();
41
- let added = false;
102
+ const fresh = [];
42
103
  for (const [port, pid] of after) {
43
104
  if (!before.has(port) && !this.servers.has(port)) {
44
- this.servers.set(port, { pid, since: Date.now() });
45
- added = true;
46
- // Best-effort process name + full command line so the panel shows
47
- // something readable (name) AND what is actually running (command).
48
- void lookupProcessName(pid).then((name) => {
49
- const cur = this.servers.get(port);
50
- if (cur && cur.pid === pid && name) {
51
- cur.name = name;
52
- this.push();
53
- }
54
- });
55
- void lookupProcessCommandLine(pid).then((command) => {
56
- const cur = this.servers.get(port);
57
- if (cur && cur.pid === pid && command) {
58
- cur.command = command;
59
- this.push();
60
- }
61
- });
62
- this.opts.emit({
63
- type: "notice",
64
- level: "info",
65
- text: `检测到 AI 启动的后台服务:端口 ${port}(pid ${pid})——可在顶栏「后台任务」里单独停止或全部关闭`,
66
- textEn: `Detected an AI-started background service: port ${port} (pid ${pid}) — stop it individually or all at once under Background tasks in the top bar`,
67
- });
105
+ fresh.push({ port, pid });
68
106
  }
69
107
  }
108
+ if (fresh.length === 0)
109
+ return;
110
+ // 剔除桌面软件误报(微信等)后,剩下的才是 AI 启动的后台服务。
111
+ const keep = await this.filterAgentSpawned(fresh);
112
+ let added = false;
113
+ for (const { port, pid } of keep) {
114
+ if (this.servers.has(port))
115
+ continue;
116
+ this.servers.set(port, { pid, since: Date.now() });
117
+ added = true;
118
+ // Best-effort process name + full command line so the panel shows
119
+ // something readable (name) AND what is actually running (command).
120
+ void lookupProcessName(pid).then((name) => {
121
+ const cur = this.servers.get(port);
122
+ if (cur && cur.pid === pid && name) {
123
+ cur.name = name;
124
+ this.push();
125
+ }
126
+ });
127
+ void lookupProcessCommandLine(pid).then((command) => {
128
+ const cur = this.servers.get(port);
129
+ if (cur && cur.pid === pid && command) {
130
+ cur.command = command;
131
+ this.push();
132
+ }
133
+ });
134
+ this.opts.emit({
135
+ type: "notice",
136
+ level: "info",
137
+ text: `检测到 AI 启动的后台服务:端口 ${port}(pid ${pid})——可在顶栏「后台任务」里单独停止或全部关闭`,
138
+ textEn: `Detected an AI-started background service: port ${port} (pid ${pid}) — stop it individually or all at once under Background tasks in the top bar`,
139
+ });
140
+ }
70
141
  if (added)
71
142
  this.push();
72
143
  }
144
+ /** 并行拉进程树与各进程名,用黑名单 + 父链回溯剔除桌面软件误报。 */
145
+ async filterAgentSpawned(fresh) {
146
+ const parents = await snapshotProcessParents();
147
+ const names = await Promise.all(fresh.map((f) => lookupProcessName(f.pid)));
148
+ const newPids = new Set(fresh.map((f) => f.pid));
149
+ const out = [];
150
+ for (let i = 0; i < fresh.length; i++) {
151
+ const { port, pid } = fresh[i];
152
+ if (shouldTrackBackgroundServer(pid, parents, newPids, process.pid, names[i])) {
153
+ out.push({ port, pid });
154
+ }
155
+ }
156
+ return out;
157
+ }
73
158
  /** The current background-server list, oldest first. 合并插件任务。 */
74
159
  list() {
75
160
  const out = [...this.servers.entries()]
@@ -193,6 +193,7 @@ export class ClientStateStore {
193
193
  visionBridgeModel: s?.settings?.visionBridgeModel ?? null,
194
194
  visionBridgePromptMode: s?.settings?.visionBridgePromptMode === "replace" ? "replace" : "append",
195
195
  visionBridgePrompt: s?.settings?.visionBridgePrompt ?? "",
196
+ subagentDefaultModel: s?.settings?.subagentDefaultModel ?? null,
196
197
  reviewPrompt: s?.settings?.reviewPrompt ?? "",
197
198
  reviewDisabledSkills: s?.settings?.reviewDisabledSkills ?? [],
198
199
  disabledPlugins: s?.settings?.disabledPlugins ?? [],
@@ -215,6 +216,7 @@ export class ClientStateStore {
215
216
  toolsWrap: settings.toolsWrap ?? cur.toolsWrap ?? true,
216
217
  visionBridgeEnabled: settings.visionBridgeEnabled ?? cur.visionBridgeEnabled ?? true,
217
218
  visionBridgeModel: settings.visionBridgeModel ?? cur.visionBridgeModel ?? null,
219
+ subagentDefaultModel: settings.subagentDefaultModel ?? cur.subagentDefaultModel ?? null,
218
220
  visionBridgePromptMode: settings.visionBridgePromptMode ?? cur.visionBridgePromptMode ?? "append",
219
221
  visionBridgePrompt: settings.visionBridgePrompt ?? cur.visionBridgePrompt ?? "",
220
222
  reviewPrompt: settings.reviewPrompt ?? cur.reviewPrompt ?? "",
@@ -2075,6 +2075,8 @@ export class DshClientSession {
2075
2075
  markers: [],
2076
2076
  subagentTemplates: [],
2077
2077
  subagentDefaultTemplates: [],
2078
+ subagentDefaultModel: null,
2079
+ subagentModels: [],
2078
2080
  };
2079
2081
  this.emit({ type: "settings_state", settings });
2080
2082
  }
@@ -823,6 +823,7 @@ wss.on("connection", (ws) => {
823
823
  visionBridgeModel: msg.visionBridgeModel,
824
824
  visionBridgePromptMode: msg.visionBridgePromptMode,
825
825
  visionBridgePrompt: msg.visionBridgePrompt,
826
+ subagentDefaultModel: msg.subagentDefaultModel,
826
827
  reviewPrompt: msg.reviewPrompt,
827
828
  reviewDisabledSkills: msg.reviewDisabledSkills,
828
829
  markersEnabled: msg.markersEnabled,
@@ -45,6 +45,47 @@ export async function snapshotListeningPorts() {
45
45
  }
46
46
  return m;
47
47
  }
48
+ /**
49
+ * Snapshot every process's parent pid → Map<pid, ppid>. Windows: one
50
+ * PowerShell CIM pass (avoids spawning PowerShell once per pid); POSIX:
51
+ * one `ps -Ao pid=,ppid=` pass. Returns undefined when the whole query
52
+ * fails — callers should treat that as "cannot verify, stay conservative".
53
+ */
54
+ export async function snapshotProcessParents() {
55
+ try {
56
+ const { execFile } = await import("node:child_process");
57
+ const m = new Map();
58
+ if (process.platform === "win32") {
59
+ const out = await new Promise((resolve, reject) => execFile("powershell.exe", [
60
+ "-NoProfile",
61
+ "-NonInteractive",
62
+ "-Command",
63
+ 'Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId):$($_.ParentProcessId)" }',
64
+ ], { windowsHide: true, timeout: 10000 }, (err, stdout) => (err ? reject(err) : resolve(stdout))));
65
+ for (const line of out.split(/\r?\n/)) {
66
+ const [pid, ppid] = line.trim().split(":").map(Number);
67
+ if (Number.isFinite(pid) && Number.isFinite(ppid))
68
+ m.set(pid, ppid);
69
+ }
70
+ }
71
+ else {
72
+ const out = await new Promise((resolve, reject) => execFile("ps", ["-Ao", "pid=,ppid="], { timeout: 5000 }, (err, stdout) => err ? reject(err) : resolve(stdout)));
73
+ for (const line of out.split(/\r?\n/)) {
74
+ const p = line.trim().split(/\s+/);
75
+ if (p.length >= 2) {
76
+ const pid = Number(p[0]);
77
+ const ppid = Number(p[1]);
78
+ if (Number.isFinite(pid) && Number.isFinite(ppid))
79
+ m.set(pid, ppid);
80
+ }
81
+ }
82
+ }
83
+ return m;
84
+ }
85
+ catch {
86
+ return undefined;
87
+ }
88
+ }
48
89
  /** Kill a pid and its whole process tree (cross-platform). */
49
90
  export function killPidTree(pid) {
50
91
  try {
@@ -8,4 +8,4 @@
8
8
  * its own copy in web/src/protocol-version.ts; scripts/check-protocol-sync.mjs
9
9
  * verifies the two never drift.
10
10
  */
11
- export const PROTOCOL_VERSION = 11;
11
+ export const PROTOCOL_VERSION = 12;
@@ -219,6 +219,8 @@ export class SettingsService {
219
219
  }),
220
220
  subagentTemplates: this.templates.list(),
221
221
  subagentDefaultTemplates: DEFAULT_TEMPLATES.map((t) => t.name),
222
+ subagentDefaultModel: this.settings.subagentDefaultModel ?? null,
223
+ subagentModels: this.collectSubagentModels(),
222
224
  },
223
225
  });
224
226
  }
@@ -236,6 +238,30 @@ export class SettingsService {
236
238
  return [];
237
239
  }
238
240
  }
241
+ /** 已配置鉴权的全部模型(子代理模型选择器用;跟随视觉桥的收集方式但不限视觉)。 */
242
+ collectSubagentModels() {
243
+ try {
244
+ const runtime = this.host.getSession().modelRuntime;
245
+ const out = [];
246
+ for (const p of runtime.getProviders()) {
247
+ if (!runtime.hasConfiguredAuth(p.id))
248
+ continue;
249
+ for (const m of runtime.getModels(p.id)) {
250
+ out.push({
251
+ provider: p.id,
252
+ id: m.id,
253
+ label: `${m.name ?? m.id} (${p.id})`,
254
+ });
255
+ }
256
+ }
257
+ // 稳定排序:provider 名 → 模型名。
258
+ return out.sort((a, b) => (a.provider === b.provider ? a.label.localeCompare(b.label) : a.provider.localeCompare(b.provider)));
259
+ }
260
+ catch {
261
+ // Session not ready yet — the picker stays empty until next push.
262
+ return [];
263
+ }
264
+ }
239
265
  /** Persist + apply a partial settings update (prompt text/mode, toggles). */
240
266
  async set(partial) {
241
267
  const needsReload = partial.promptMode !== undefined ||
@@ -291,6 +317,11 @@ export class SettingsService {
291
317
  if (partial.reviewDisabledSkills !== undefined) {
292
318
  this.settings.reviewDisabledSkills = partial.reviewDisabledSkills;
293
319
  }
320
+ if (partial.subagentDefaultModel !== undefined) {
321
+ // 空串归一为 null(跟随主对话);其余剥空白后存格式 provider/id。
322
+ const m = partial.subagentDefaultModel?.trim() ?? "";
323
+ this.settings.subagentDefaultModel = m ? m : null;
324
+ }
294
325
  this.host.stateStore.saveSettings(this.host.clientId, this.settings);
295
326
  this.push();
296
327
  if (needsReload)
@@ -360,6 +391,8 @@ export class SettingsService {
360
391
  visionBridgeModel: this.settings.visionBridgeModel,
361
392
  visionBridgePromptMode: this.settings.visionBridgePromptMode,
362
393
  visionBridgePrompt: this.settings.visionBridgePrompt,
394
+ // 子代理默认模型也不进预设——保留当前值。
395
+ subagentDefaultModel: this.settings.subagentDefaultModel,
363
396
  };
364
397
  this.host.stateStore.saveSettings(this.host.clientId, this.settings);
365
398
  this.push();
@@ -2,7 +2,9 @@
2
2
  * subagent-templates.ts — 子代理模板库(全局共享,<dataDir>/subagent-templates.json)。
3
3
  *
4
4
  * 模板 = 派生子代理时套用的预设:角色系统提示词(replace/append 同主设置语义)+
5
- * 技能白名单 + 扩展白名单。白名单空数组 = 该维度不限定,子代理跟随主会话设置。
5
+ * 技能白名单 + 扩展白名单 + 可选模型。白名单空数组 = 该维度不限定,子代理跟随主会话设置。
6
+ * `model` 为 "provider/id"(与 subagent_spawn 的 model 参数、设置面板子代理默认模型
7
+ * 同格式);空字符串 = 跟随主对话当前模型。
6
8
  * 带 `enabled: false` 的模板停用:设置面板仍可见、可重新启用,但 AI 工具
7
9
  * (subagent_templates / subagent_spawn)查询不到它、也不能选择它——「关闭 =
8
10
  * 对 AI 不可见」。
@@ -14,6 +16,8 @@ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
14
16
  import { dirname } from "node:path";
15
17
  /** 名字去空白折叠后非空且 ≤ 60 字符(工具参数可读,允许中文)。 */
16
18
  const NAME_MAX = 60;
19
+ /** 模型 id 上限("provider/id",含斜杠与自定义模型目录 id)。 */
20
+ const MODEL_MAX = 200;
17
21
  /**
18
22
  * 内置默认模板(第一次运行时种子进列表;用户改动后以 <dataDir> 文件为准)。
19
23
  * 文案改编自 pi-subagents 社区项目(tintinweb / nicobailon)的角色提示词,
@@ -21,6 +25,7 @@ const NAME_MAX = 60;
21
25
  * 白名单留空 = 技能/扩展跟随主会话设置,开箱即用。
22
26
  */
23
27
  export const DEFAULT_TEMPLATES = [
28
+ // 全部内置模板默认 model 为空字符串 = 跟随主对话模型(面板改模板时可指定专属模型)。
24
29
  {
25
30
  name: "review",
26
31
  description: "代码 / 计划 / 方案 / PR 审查:有证据的 P0-P2 发现与合并结论",
@@ -46,6 +51,7 @@ export const DEFAULT_TEMPLATES = [
46
51
  "\n只报告由目标改动引起或可达的具体当前问题,并为每条提供源码证明、测试/复现或契约冲突。P0 = 阻止合并;P1 = 发布前应修;P2 = 仅报告。没有符合条件的问题时,明确写「No issues found.」。",
47
52
  enabledSkills: [],
48
53
  enabledExtensions: [],
54
+ model: "",
49
55
  enabled: true,
50
56
  },
51
57
  {
@@ -71,6 +77,7 @@ export const DEFAULT_TEMPLATES = [
71
77
  "实现内容:X。\n改动文件:Y。\n验证:Z。\n未决风险/问题:R。\n建议下一步:N。",
72
78
  enabledSkills: [],
73
79
  enabledExtensions: [],
80
+ model: "",
74
81
  enabled: true,
75
82
  },
76
83
  {
@@ -94,6 +101,7 @@ export const DEFAULT_TEMPLATES = [
94
101
  "## Gaps\n未能自信回答的部分与建议下一步。",
95
102
  enabledSkills: [],
96
103
  enabledExtensions: [],
104
+ model: "",
97
105
  enabled: true,
98
106
  },
99
107
  {
@@ -119,6 +127,7 @@ export const DEFAULT_TEMPLATES = [
119
127
  "## Start Here\n另一个 agent 应最先打开的文件及原因。",
120
128
  enabledSkills: [],
121
129
  enabledExtensions: [],
130
+ model: "",
122
131
  enabled: true,
123
132
  },
124
133
  {
@@ -135,6 +144,7 @@ export const DEFAULT_TEMPLATES = [
135
144
  "只报告能由证据支持的发现,不要臆测。没有发现问题时明确说「未发现安全问题」。保持简洁。",
136
145
  enabledSkills: [],
137
146
  enabledExtensions: [],
147
+ model: "",
138
148
  enabled: true,
139
149
  },
140
150
  {
@@ -148,6 +158,7 @@ export const DEFAULT_TEMPLATES = [
148
158
  "- 简洁、具体,引用证据(路径、命令输出、行号)。",
149
159
  enabledSkills: [],
150
160
  enabledExtensions: [],
161
+ model: "",
151
162
  enabled: true,
152
163
  },
153
164
  ];
@@ -170,6 +181,8 @@ function normalize(raw) {
170
181
  enabledExtensions: Array.isArray(o.enabledExtensions)
171
182
  ? o.enabledExtensions.filter((x) => typeof x === "string")
172
183
  : [],
184
+ // 空字符串 = 跟随主对话;其余剥掉首尾空白,超长当脏数据丢弃。
185
+ model: typeof o.model === "string" ? o.model.trim() : "",
173
186
  enabled: o.enabled !== false,
174
187
  };
175
188
  }