pi-web-ui 0.71.0 → 0.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -1
- package/README.md +17 -9
- package/README.zh-CN.md +22 -11
- package/dist/server/agent-service.js +526 -71
- package/dist/server/client-state.js +56 -0
- package/dist/server/dsh/dsh-agent-service.js +99 -3
- package/dist/server/index.js +11 -1
- package/dist/server/model-admin.js +135 -23
- package/dist/server/patch-remote-catalog.js +59 -0
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/subagents.js +51 -4
- package/package.json +4 -1
- package/themes/cyberpunk.css +1 -1
- package/themes/dazzle.css +1 -1
- package/themes/md-preview.css +1 -1
- package/themes/translucent.css +2 -2
- package/themes/white.css +1 -1
- package/web/dist/assets/{TerminalPanel-DXNpevIG.js → TerminalPanel-Bz-58xBx.js} +1 -1
- package/web/dist/assets/index-DB1SxjbK.js +335 -0
- package/web/dist/assets/index-Dqbw78CU.css +10 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-4ge_ioPK.css +0 -10
- package/web/dist/assets/index-Cue012aF.js +0 -335
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
* snapshots. The frontend is snapshot-driven (server is the source of truth),
|
|
11
11
|
* so reconnects just re-request a snapshot.
|
|
12
12
|
*/
|
|
13
|
+
// MUST be the first import: rewrites the SDK's installed remote-catalog
|
|
14
|
+
// provider so built-in model lists follow the official pi.dev catalog
|
|
15
|
+
// wholesale (no union merge / no stale built-in leftovers).
|
|
16
|
+
import "./patch-remote-catalog.js";
|
|
13
17
|
import { spawn } from "node:child_process";
|
|
14
18
|
import { randomUUID } from "node:crypto";
|
|
15
19
|
import { createRequire } from "node:module";
|
|
@@ -34,7 +38,7 @@ import { SubagentTemplatesStore, pickTemplatePrompt } from "./subagent-templates
|
|
|
34
38
|
import { applyHeadTail, makePersistentTerminalTools, makeTerminalBashTool, stripAnsi, TERMINAL_TOOLS_GUIDANCE, TERMINAL_TOOL_NAMES, } from "./terminals.js";
|
|
35
39
|
import { WebUIContext, mockThemeProxy } from "./webui-context.js";
|
|
36
40
|
import { makeEditSoftTool, SOFT_EDIT_TOOL_NAME } from "./edit-soft-tool.js";
|
|
37
|
-
import { makeSubagentTools, subagentTitle, withSubagentOwner, } from "./subagents.js";
|
|
41
|
+
import { collectSubagentDescendantIds, makeSubagentTools, subagentTitle, withSubagentOwner, } from "./subagents.js";
|
|
38
42
|
import { buildAttachmentMessages } from "./attachments.js";
|
|
39
43
|
import { BUILTIN_SOUL, DEFAULT_PROMPT_TEMPLATE, buildToolsSchemaText, renderPromptTemplate, resolveSectionTexts, } from "./prompt-composer.js";
|
|
40
44
|
import { serializeMessage, serializeStreamingMessage, stripTransientRetryErrors, } from "./serialize.js";
|
|
@@ -400,8 +404,9 @@ const TOOL_WATCHDOG_TIMEOUT_MS = (() => {
|
|
|
400
404
|
const v = Number(process.env.PI_WEB_TOOL_TIMEOUT_MS);
|
|
401
405
|
return Number.isFinite(v) && v > 0 ? v : 20 * 60_000;
|
|
402
406
|
})();
|
|
403
|
-
/** Cap on simultaneously open conversations of ONE project (each keeps a full
|
|
404
|
-
* runtime alive; conversations of other projects keep their own lists).
|
|
407
|
+
/** Cap on simultaneously open NON-subagent conversations of ONE project (each keeps a full
|
|
408
|
+
* runtime alive; conversations of other projects keep their own lists).
|
|
409
|
+
* 子代理不计入:子代理是 inMemory 后台任务,不参与此上限,既不占位也不被此上限拦截。 */
|
|
405
410
|
const MAX_OPEN_CONVERSATIONS = 8;
|
|
406
411
|
const DEFAULT_CONV_TITLE = "新对话";
|
|
407
412
|
/** First user text in a session, truncated for the conversation list. */
|
|
@@ -724,6 +729,8 @@ export class ClientSession {
|
|
|
724
729
|
conv.listed = true;
|
|
725
730
|
conv.title = subagentTitle(prompt);
|
|
726
731
|
this.convs.set(conv.id, conv);
|
|
732
|
+
// 子代理不走 bindSession——这里同样注入面板的重试次数覆盖。
|
|
733
|
+
this.applyRetryOverrides();
|
|
727
734
|
// 扩展绑定(rpc 模式;uiContext 只给无害的 mock theme/status 槽,避免与主对话
|
|
728
735
|
// 的 widget 冲突。无 uiContext 时扩展的 ctx.ui.theme.fg 会打到 TUI 真 theme
|
|
729
736
|
// 代理上抛 "Theme not initialized",每个扩展一条 error toast。)
|
|
@@ -1382,9 +1389,11 @@ export class ClientSession {
|
|
|
1382
1389
|
// 父对话 = 真正调用 spawn 的那个会话(本 runtime 所属会话),而不是
|
|
1383
1390
|
// 派发瞬间的 active——后台对话继续产出时用户可能已切到别的项目,用
|
|
1384
1391
|
// activeId 会把孩子记到无关会话名下、沉到别的组/底部(issue #95)。
|
|
1385
|
-
// ownerId 即本 runtime
|
|
1392
|
+
// ownerId 即本 runtime 所属会话(创建时就已知,见各调用点),一身二任:
|
|
1393
|
+
// spawn 的 parentId(子代理记到真正的派发会话名下)+ wait_all 的
|
|
1394
|
+
// selfConvId(调用者自身永不计入等待,防 self-wait deadlock)。
|
|
1386
1395
|
...(ownerId
|
|
1387
|
-
? makeSubagentTools(withSubagentOwner(this.subagentHost, ownerId))
|
|
1396
|
+
? makeSubagentTools(withSubagentOwner(this.subagentHost, ownerId), undefined, ownerId)
|
|
1388
1397
|
: makeSubagentTools(this.subagentHost)),
|
|
1389
1398
|
// 内置标记只读查询工具(todo/svc 状态查询,写操作走内联标记)。
|
|
1390
1399
|
makeMarkersListTool(() => this.activeId, this.markerSvc),
|
|
@@ -1535,6 +1544,10 @@ export class ClientSession {
|
|
|
1535
1544
|
},
|
|
1536
1545
|
});
|
|
1537
1546
|
conv.unsubscribe = conv.session.subscribe((event) => this.onEvent(conv, event));
|
|
1547
|
+
// 新会话 / 切换会话 / 强杀重建的必经之路:刚创建的 runtime 用的是 SDK
|
|
1548
|
+
// 默认重试 3 次——这里把面板的 retryMaxAttempts 覆盖注入,否则“设了 6
|
|
1549
|
+
// 次还是按 3 次重试”。已存在会话重复注入是幂等的(同值覆盖)。
|
|
1550
|
+
this.applyRetryOverrides();
|
|
1538
1551
|
this.scheduleSnapshot();
|
|
1539
1552
|
this.webUi.refresh();
|
|
1540
1553
|
this.startWidgetsTimer();
|
|
@@ -2671,8 +2684,11 @@ export class ClientSession {
|
|
|
2671
2684
|
setProviderApiKey(provider, apiKey) {
|
|
2672
2685
|
return this.modelAdmin.setProviderApiKey(provider, apiKey);
|
|
2673
2686
|
}
|
|
2674
|
-
clearProviderApiKey(provider) {
|
|
2675
|
-
|
|
2687
|
+
async clearProviderApiKey(provider) {
|
|
2688
|
+
await this.modelAdmin.clearProviderApiKey(provider);
|
|
2689
|
+
// The provider is back to unconfigured — drop its key preference in
|
|
2690
|
+
// EVERY project, otherwise each project switch re-tries a restore.
|
|
2691
|
+
this.stateStore.deleteProviderEverywhere(provider.trim());
|
|
2676
2692
|
}
|
|
2677
2693
|
listProviders() {
|
|
2678
2694
|
return this.modelAdmin.listProviders();
|
|
@@ -2680,6 +2696,9 @@ export class ClientSession {
|
|
|
2680
2696
|
listModelsConfig() {
|
|
2681
2697
|
return this.modelAdmin.listModelsConfig();
|
|
2682
2698
|
}
|
|
2699
|
+
reloadModelsConfig() {
|
|
2700
|
+
return this.modelAdmin.reloadModelsConfig();
|
|
2701
|
+
}
|
|
2683
2702
|
fetchModelsList(reqId, baseUrl, apiKey, authHeader, api) {
|
|
2684
2703
|
return this.modelAdmin.fetchModelsList(reqId, baseUrl, apiKey, authHeader, api, () => this.getLang());
|
|
2685
2704
|
}
|
|
@@ -2708,23 +2727,27 @@ export class ClientSession {
|
|
|
2708
2727
|
this.stateStore.saveProjectProviderKey(this.clientId, this.cwd, provider, active);
|
|
2709
2728
|
}
|
|
2710
2729
|
async activateProviderKey(provider, keyName) {
|
|
2711
|
-
await this.modelAdmin.activateProviderKey(provider, keyName);
|
|
2712
|
-
|
|
2730
|
+
const ok = await this.modelAdmin.activateProviderKey(provider, keyName);
|
|
2731
|
+
// Only remember existing keys — a failed switch (deleted key) must not
|
|
2732
|
+
// plant a stale reference that errors on every later project switch.
|
|
2733
|
+
if (ok)
|
|
2734
|
+
this.stateStore.saveProjectProviderKey(this.clientId, this.cwd, provider, keyName);
|
|
2735
|
+
else
|
|
2736
|
+
this.stateStore.deleteProjectProviderKey(this.clientId, this.cwd, provider);
|
|
2713
2737
|
}
|
|
2714
2738
|
async removeProviderKey(provider, keyName) {
|
|
2715
2739
|
await this.modelAdmin.removeProviderKey(provider, keyName);
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
else
|
|
2722
|
-
this.stateStore.deleteProjectProviderKey(this.clientId, this.cwd, provider);
|
|
2723
|
-
}
|
|
2740
|
+
// The deletion may have been made from another project: every project
|
|
2741
|
+
// still pinned to the deleted key must follow the key that took over
|
|
2742
|
+
// (or drop the pin when no keys remain), not just the current one.
|
|
2743
|
+
const active = this.modelAdmin.getActiveKeyName(provider);
|
|
2744
|
+
this.stateStore.repointDeletedKeyEverywhere(provider, keyName, active);
|
|
2724
2745
|
}
|
|
2725
2746
|
/** Restore per-project provider keys when entering a project. For each
|
|
2726
2747
|
* provider that has a saved key for `cwd`, activate it if it differs from
|
|
2727
|
-
* the current global active. Silent
|
|
2748
|
+
* the current global active. Silent + self-healing: a saved key deleted
|
|
2749
|
+
* elsewhere is dropped without notifying (a noisy error here is what
|
|
2750
|
+
* haunted project switches after a key deletion). */
|
|
2728
2751
|
async restoreProjectProviderKeysForCwd(cwd) {
|
|
2729
2752
|
const saved = this.stateStore.getProjectProviderKeys(this.clientId, cwd);
|
|
2730
2753
|
if (!saved)
|
|
@@ -2733,15 +2756,17 @@ export class ClientSession {
|
|
|
2733
2756
|
const cur = this.modelAdmin.getActiveKeyName(provider);
|
|
2734
2757
|
if (cur === keyName)
|
|
2735
2758
|
continue;
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
catch {
|
|
2740
|
-
// saved key may have been deleted — ignore
|
|
2759
|
+
if (!this.modelAdmin.hasProviderKey(provider, keyName)) {
|
|
2760
|
+
this.stateStore.deleteProjectProviderKey(this.clientId, cwd, provider);
|
|
2761
|
+
continue;
|
|
2741
2762
|
}
|
|
2763
|
+
const ok = await this.modelAdmin.activateProviderKey(provider, keyName, { silent: true });
|
|
2764
|
+
if (!ok)
|
|
2765
|
+
this.stateStore.deleteProjectProviderKey(this.clientId, cwd, provider);
|
|
2742
2766
|
}
|
|
2743
2767
|
}
|
|
2744
|
-
/** When a model is set, ensure its provider's per-project key is restored.
|
|
2768
|
+
/** When a model is set, ensure its provider's per-project key is restored.
|
|
2769
|
+
* Silent + self-healing like the bulk restore above. */
|
|
2745
2770
|
async restoreKeyForModel(modelId, cwd) {
|
|
2746
2771
|
const slash = modelId.indexOf("/");
|
|
2747
2772
|
if (slash <= 0)
|
|
@@ -2753,10 +2778,13 @@ export class ClientSession {
|
|
|
2753
2778
|
const cur = this.modelAdmin.getActiveKeyName(provider);
|
|
2754
2779
|
if (cur === saved)
|
|
2755
2780
|
return;
|
|
2756
|
-
|
|
2757
|
-
|
|
2781
|
+
if (!this.modelAdmin.hasProviderKey(provider, saved)) {
|
|
2782
|
+
this.stateStore.deleteProjectProviderKey(this.clientId, cwd, provider);
|
|
2783
|
+
return;
|
|
2758
2784
|
}
|
|
2759
|
-
|
|
2785
|
+
const ok = await this.modelAdmin.activateProviderKey(provider, saved, { silent: true });
|
|
2786
|
+
if (!ok)
|
|
2787
|
+
this.stateStore.deleteProjectProviderKey(this.clientId, cwd, provider);
|
|
2760
2788
|
}
|
|
2761
2789
|
/** Remember the just-selected model (and the key that was active for its
|
|
2762
2790
|
* provider) for the current project. Called IMMEDIATELY on model selection —
|
|
@@ -3129,6 +3157,87 @@ export class ClientSession {
|
|
|
3129
3157
|
await this.interruptRun(this.conv, "已停止");
|
|
3130
3158
|
this.flushSnapshot();
|
|
3131
3159
|
}
|
|
3160
|
+
/** 手动重试上次失败的模型调用:自动重试次数(retryMaxAttempts)用完后
|
|
3161
|
+
* 本轮已停止并标红,用户点「重试」再触发一轮 LLM 调用。不新增用户气泡——
|
|
3162
|
+
* 用 display:false 的 custom 消息 triggerTurn 续跑,模型基于完整上下文
|
|
3163
|
+
* (含上次报错)继续生成。流式中 / 无可重试失败时只发 notice 拒绝。 */
|
|
3164
|
+
async retryLast() {
|
|
3165
|
+
const conv = this.conv;
|
|
3166
|
+
try {
|
|
3167
|
+
if (this.quiesceBlocked())
|
|
3168
|
+
return;
|
|
3169
|
+
const s = this.session;
|
|
3170
|
+
if (s.isStreaming) {
|
|
3171
|
+
this.emit({
|
|
3172
|
+
type: "notice",
|
|
3173
|
+
level: "info",
|
|
3174
|
+
text: "对话正在生成中,无需重试",
|
|
3175
|
+
textEn: "The conversation is still generating — no need to retry",
|
|
3176
|
+
});
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
if (conv.retryState) {
|
|
3180
|
+
this.emit({
|
|
3181
|
+
type: "notice",
|
|
3182
|
+
level: "info",
|
|
3183
|
+
text: "正在自动重试中,稍候即可",
|
|
3184
|
+
textEn: "Auto-retry is in progress — please wait",
|
|
3185
|
+
});
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
// 最后一轮失败的证据:末尾 stopReason=error 的 assistant 消息。
|
|
3189
|
+
let failed = null;
|
|
3190
|
+
try {
|
|
3191
|
+
const msgs = s.agent.state.messages;
|
|
3192
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
3193
|
+
const m = msgs[i];
|
|
3194
|
+
if (m.role !== "assistant")
|
|
3195
|
+
continue;
|
|
3196
|
+
if ((typeof m.errorMessage === "string" && m.errorMessage.trim()) || m.stopReason === "error") {
|
|
3197
|
+
failed = m;
|
|
3198
|
+
}
|
|
3199
|
+
break;
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
catch {
|
|
3203
|
+
// 会话替换中——按无可重试处理
|
|
3204
|
+
}
|
|
3205
|
+
if (!failed) {
|
|
3206
|
+
this.emit({
|
|
3207
|
+
type: "notice",
|
|
3208
|
+
level: "info",
|
|
3209
|
+
text: "没有可重试的失败:上一轮没有报错结束",
|
|
3210
|
+
textEn: "Nothing to retry: the last turn did not end with an error",
|
|
3211
|
+
});
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
// 轨迹用:下一轮 agent_start 消费(否则插件回退为「继续执行」)。
|
|
3215
|
+
conv.pendingTask = "手动重试上次失败的模型请求";
|
|
3216
|
+
await s.sendCustomMessage({
|
|
3217
|
+
customType: "manual-retry",
|
|
3218
|
+
content: [
|
|
3219
|
+
{
|
|
3220
|
+
type: "text",
|
|
3221
|
+
text: "(系统:用户点击了「重试」。请基于完整上下文重新发起上一次失败的模型请求,继续完成用户的任务。)",
|
|
3222
|
+
},
|
|
3223
|
+
],
|
|
3224
|
+
display: false,
|
|
3225
|
+
}, { triggerTurn: true });
|
|
3226
|
+
conv.promptedSinceActive = true;
|
|
3227
|
+
conv.lastActiveAt = Date.now();
|
|
3228
|
+
conv.lastSdkEventAt = Date.now();
|
|
3229
|
+
conv.stallNoticed = false;
|
|
3230
|
+
}
|
|
3231
|
+
catch (err) {
|
|
3232
|
+
this.emit({
|
|
3233
|
+
type: "notice",
|
|
3234
|
+
level: "error",
|
|
3235
|
+
text: `手动重试失败:${err.message}`,
|
|
3236
|
+
textEn: `Manual retry failed: ${err.message}`,
|
|
3237
|
+
});
|
|
3238
|
+
}
|
|
3239
|
+
this.flushSnapshot();
|
|
3240
|
+
}
|
|
3132
3241
|
/**
|
|
3133
3242
|
* Remove ONE queued prompt text (the ✕ on a pending bubble) so it is neither
|
|
3134
3243
|
* shown nor eventually delivered. The pi SDK has no per-item queue API, so we
|
|
@@ -3379,8 +3488,9 @@ export class ClientSession {
|
|
|
3379
3488
|
}
|
|
3380
3489
|
}
|
|
3381
3490
|
// Cap is per project — conversations of other projects keep their own
|
|
3382
|
-
// lists and don't consume this project's slots.
|
|
3383
|
-
|
|
3491
|
+
// lists and don't consume this project's slots. Subagents don't count
|
|
3492
|
+
// (inMemory 后台任务,不占位)。
|
|
3493
|
+
const openInProject = [...this.convs.values()].filter((c) => c.cwd === this.cwd && !c.isSubagent).length;
|
|
3384
3494
|
if (openInProject >= MAX_OPEN_CONVERSATIONS) {
|
|
3385
3495
|
this.emit({
|
|
3386
3496
|
type: "notice",
|
|
@@ -3450,6 +3560,9 @@ export class ClientSession {
|
|
|
3450
3560
|
* The active conversation is being left (new_chat / switch_conversation /
|
|
3451
3561
|
* set_cwd). Runs the running-list lifecycle:
|
|
3452
3562
|
*
|
|
3563
|
+
* - 子代理豁免:活动的是子代理时永远保留(listed=true,返回 null)——点开
|
|
3564
|
+
* 查看后切走也不释放 runtime,后台任务继续跑、随时可点开看;清理走
|
|
3565
|
+
* dismiss_conversation / dismiss_finished_subagents(用户显式动作)。
|
|
3453
3566
|
* - still streaming → it becomes a background run: ensure it is listed;
|
|
3454
3567
|
* - idle + listed + continued → keep it (the user did continue it);
|
|
3455
3568
|
* - any retained terminal state → keep it listed until the terminals are closed;
|
|
@@ -3459,6 +3572,11 @@ export class ClientSession {
|
|
|
3459
3572
|
*/
|
|
3460
3573
|
displaceActive() {
|
|
3461
3574
|
const conv = this.conv;
|
|
3575
|
+
// 子代理不受切换关闭影响(见上)。
|
|
3576
|
+
if (conv.isSubagent) {
|
|
3577
|
+
conv.listed = true;
|
|
3578
|
+
return null;
|
|
3579
|
+
}
|
|
3462
3580
|
// An isolated reviewer can keep working while the main session is idle;
|
|
3463
3581
|
// retain that conversation so its review is not disposed when the user
|
|
3464
3582
|
// switches away without sending another prompt.
|
|
@@ -3844,11 +3962,40 @@ export class ClientSession {
|
|
|
3844
3962
|
if (changed)
|
|
3845
3963
|
this.emitConversations();
|
|
3846
3964
|
}
|
|
3965
|
+
/** Dismiss 口径的「已结束子代理」:非 streaming 且无保留态(存活终端/
|
|
3966
|
+
* 审查/后台唤醒等),与 dismissFinishedSubagents 的候选口径一致。 */
|
|
3967
|
+
isDismissableFinishedSubagent(conv) {
|
|
3968
|
+
let streaming = true;
|
|
3969
|
+
try {
|
|
3970
|
+
streaming = conv.session.isStreaming;
|
|
3971
|
+
}
|
|
3972
|
+
catch {
|
|
3973
|
+
// 会话替换中——按运行中处理,绝不误删。
|
|
3974
|
+
}
|
|
3975
|
+
if (streaming)
|
|
3976
|
+
return false;
|
|
3977
|
+
return !shouldRetainActive({
|
|
3978
|
+
reviewing: conv.goal.reviewing,
|
|
3979
|
+
wizardRunning: conv.wizardRunning,
|
|
3980
|
+
streaming: false,
|
|
3981
|
+
openTerminals: conv.terminals.countLive(),
|
|
3982
|
+
listed: false,
|
|
3983
|
+
promptedSinceActive: false,
|
|
3984
|
+
hasActiveSubagentRun: () => hasActiveSubagentRun({ sessionId: conv.session.sessionFile }),
|
|
3985
|
+
hasPendingWake: () => hasPendingWaitSubscription({ sessionId: conv.session.sessionFile }),
|
|
3986
|
+
});
|
|
3987
|
+
}
|
|
3847
3988
|
/** Dismiss a running conversation from the left-panel list without deleting its
|
|
3848
3989
|
* transcript file. Only idle (non-streaming) conversations that are not
|
|
3849
3990
|
* retained by terminal/wake/review state can be dismissed. The session stays
|
|
3850
|
-
* in history and can be reopened.
|
|
3851
|
-
|
|
3991
|
+
* in history and can be reopened.
|
|
3992
|
+
*
|
|
3993
|
+
* withFinishedSubagents=true 时连带关闭该对话下已结束的子代理(传递后代,
|
|
3994
|
+
* 与 dismissFinishedSubagents 同口径;active 的子代理跳过)——只关不运行的:
|
|
3995
|
+
* 运行中的后代不受影响;关完后若还有后代剩下(运行中/保留中/active),父级
|
|
3996
|
+
* 暂留并提示。只有运行中的后代(无可关的)时拒绝。不传 + 存在已结束子代理
|
|
3997
|
+
* 后代时拒绝并提示(由前端确认框先问用户,避免静默 orphan)。 */
|
|
3998
|
+
async dismissConversation(id, withFinishedSubagents, force) {
|
|
3852
3999
|
const conv = this.convs.get(id);
|
|
3853
4000
|
if (!conv) {
|
|
3854
4001
|
this.emit({
|
|
@@ -3859,72 +4006,363 @@ export class ClientSession {
|
|
|
3859
4006
|
});
|
|
3860
4007
|
return;
|
|
3861
4008
|
}
|
|
3862
|
-
if (
|
|
4009
|
+
if (!conv.listed) {
|
|
4010
|
+
// Not in list anyway — nothing to do.
|
|
4011
|
+
this.emitConversations();
|
|
4012
|
+
return;
|
|
4013
|
+
}
|
|
4014
|
+
// Streaming / retained conversations refuse dismissal — mirrors displaceActive retention.
|
|
4015
|
+
// 运行中的子代理后代也阻止关闭(绝不连带 abort);已结束的子代理后代:
|
|
4016
|
+
// withFinishedSubagents 才连带,否则拒绝并提示(前端确认框先问用户)。
|
|
4017
|
+
const isStreaming = (c) => {
|
|
4018
|
+
try {
|
|
4019
|
+
return c.session.isStreaming;
|
|
4020
|
+
}
|
|
4021
|
+
catch {
|
|
4022
|
+
return true;
|
|
4023
|
+
}
|
|
4024
|
+
};
|
|
4025
|
+
const descendants = collectSubagentDescendantIds([...this.convs.values()].map((c) => ({ id: c.id, parentId: c.parentId, isSubagent: c.isSubagent })), id)
|
|
4026
|
+
.map((did) => this.convs.get(did))
|
|
4027
|
+
.filter((c) => !!c);
|
|
4028
|
+
if (force) {
|
|
4029
|
+
await this.forceDismissConversation(conv, descendants, isStreaming);
|
|
4030
|
+
return;
|
|
4031
|
+
}
|
|
4032
|
+
const runningKids = descendants.filter((c) => isStreaming(c));
|
|
4033
|
+
// 父对话自身的保留态(流式/终端/审查/后台唤醒)——子代理后代另算。
|
|
4034
|
+
const selfStreaming = (() => {
|
|
4035
|
+
try {
|
|
4036
|
+
return conv.session.isStreaming;
|
|
4037
|
+
}
|
|
4038
|
+
catch {
|
|
4039
|
+
return true;
|
|
4040
|
+
}
|
|
4041
|
+
})();
|
|
4042
|
+
if (selfStreaming) {
|
|
3863
4043
|
this.emit({
|
|
3864
4044
|
type: "notice",
|
|
3865
4045
|
level: "warning",
|
|
3866
|
-
text:
|
|
3867
|
-
textEn: "
|
|
4046
|
+
text: `对话「${conv.title}」仍在运行中,请先等待结束或点击停止后再移出`,
|
|
4047
|
+
textEn: `Conversation "${conv.title}" is still running — wait for it to finish or press Stop before removing`,
|
|
3868
4048
|
});
|
|
3869
4049
|
return;
|
|
3870
4050
|
}
|
|
3871
|
-
if (
|
|
3872
|
-
|
|
3873
|
-
|
|
4051
|
+
if (conv.terminals.countLive() > 0) {
|
|
4052
|
+
this.emit({
|
|
4053
|
+
type: "notice",
|
|
4054
|
+
level: "warning",
|
|
4055
|
+
text: `对话「${conv.title}」还有未关闭的终端,请先关闭终端后再移出`,
|
|
4056
|
+
textEn: `Conversation "${conv.title}" still has open terminals — close them before removing`,
|
|
4057
|
+
});
|
|
3874
4058
|
return;
|
|
3875
4059
|
}
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
4060
|
+
if (shouldRetainActive({
|
|
4061
|
+
reviewing: conv.goal.reviewing,
|
|
4062
|
+
wizardRunning: conv.wizardRunning,
|
|
4063
|
+
streaming: false,
|
|
4064
|
+
openTerminals: 0,
|
|
4065
|
+
listed: conv.listed,
|
|
4066
|
+
promptedSinceActive: conv.promptedSinceActive,
|
|
4067
|
+
hasActiveSubagentRun: () => hasActiveSubagentRun({ sessionId: conv.session.sessionFile }),
|
|
4068
|
+
hasPendingWake: () => hasPendingWaitSubscription({ sessionId: conv.session.sessionFile }),
|
|
4069
|
+
})) {
|
|
4070
|
+
this.emit({
|
|
4071
|
+
type: "notice",
|
|
4072
|
+
level: "warning",
|
|
4073
|
+
text: `对话「${conv.title}」暂时无法移出(存在待处理的后台任务/审查)`,
|
|
4074
|
+
textEn: `Conversation "${conv.title}" cannot be removed right now (pending background task/review)`,
|
|
3889
4075
|
});
|
|
3890
|
-
|
|
3891
|
-
|
|
4076
|
+
return;
|
|
4077
|
+
}
|
|
4078
|
+
const finishedKids = descendants.filter((c) => c.listed && c.id !== this.activeId && this.isDismissableFinishedSubagent(c));
|
|
4079
|
+
if (runningKids.length > 0 && finishedKids.length === 0) {
|
|
4080
|
+
// 只有运行中的后代:连 flag 也变不出可关的,拒绝(绝不连带 abort)。
|
|
4081
|
+
this.emit({
|
|
4082
|
+
type: "notice",
|
|
4083
|
+
level: "warning",
|
|
4084
|
+
text: `对话「${conv.title}」还有 ${runningKids.length} 个运行中的子代理,请先等待结束或停止后再移出`,
|
|
4085
|
+
textEn: `Conversation "${conv.title}" still has ${runningKids.length} running subagent(s) — wait for them to finish or stop them before removing`,
|
|
4086
|
+
});
|
|
4087
|
+
return;
|
|
4088
|
+
}
|
|
4089
|
+
if (finishedKids.length > 0 && !withFinishedSubagents) {
|
|
4090
|
+
this.emit({
|
|
4091
|
+
type: "notice",
|
|
4092
|
+
level: "warning",
|
|
4093
|
+
text: `对话「${conv.title}」下还有 ${finishedKids.length} 个已结束的子代理:连带关闭请确认,仅关闭父级请先在右键菜单清理子代理`,
|
|
4094
|
+
textEn: `Conversation "${conv.title}" still has ${finishedKids.length} finished subagent(s): confirm to dismiss them together, or clear the subagents first (right-click menu) to dismiss only the parent`,
|
|
4095
|
+
});
|
|
4096
|
+
return;
|
|
4097
|
+
}
|
|
4098
|
+
// 只关不运行的:运行中的后代绝不连带 abort;关完后若还有后代剩下
|
|
4099
|
+
// (运行中/保留中/active),父级暂留并提示。
|
|
4100
|
+
let removedKids = 0;
|
|
4101
|
+
for (const kid of finishedKids) {
|
|
4102
|
+
if (kid.id === this.activeId)
|
|
4103
|
+
continue;
|
|
4104
|
+
if (this.convs.get(kid.id) !== kid)
|
|
4105
|
+
continue;
|
|
4106
|
+
this.removeConversation(kid.id);
|
|
4107
|
+
removedKids++;
|
|
4108
|
+
}
|
|
4109
|
+
if (withFinishedSubagents && removedKids > 0) {
|
|
4110
|
+
const remaining = descendants.filter((c) => this.convs.get(c.id) === c);
|
|
4111
|
+
if (remaining.length > 0) {
|
|
4112
|
+
const stillRunning = remaining.filter((c) => isStreaming(c)).length;
|
|
3892
4113
|
this.emit({
|
|
3893
4114
|
type: "notice",
|
|
3894
|
-
level: "
|
|
3895
|
-
text:
|
|
3896
|
-
textEn: `
|
|
4115
|
+
level: "info",
|
|
4116
|
+
text: `已关闭 ${removedKids} 个已结束的子代理,还有 ${remaining.length} 个子代理未关闭${stillRunning > 0 ? `(${stillRunning} 个运行中)` : ""},父对话暂留`,
|
|
4117
|
+
textEn: `Dismissed ${removedKids} finished subagent(s); ${remaining.length} subagent(s) remain${stillRunning > 0 ? ` (${stillRunning} running)` : ""}, keeping the parent`,
|
|
3897
4118
|
});
|
|
4119
|
+
this.emitConversations();
|
|
4120
|
+
this.flushSnapshot();
|
|
4121
|
+
return;
|
|
3898
4122
|
}
|
|
3899
|
-
|
|
4123
|
+
}
|
|
4124
|
+
// Dismissing the ACTIVE conversation: move active elsewhere first
|
|
4125
|
+
// (another listed conversation, else a fresh chat), then remove.
|
|
4126
|
+
if (id === this.activeId) {
|
|
4127
|
+
const vacated = await this.vacateActive(id);
|
|
4128
|
+
if (!vacated) {
|
|
3900
4129
|
this.emit({
|
|
3901
4130
|
type: "notice",
|
|
3902
4131
|
level: "warning",
|
|
3903
|
-
text:
|
|
3904
|
-
textEn: `
|
|
4132
|
+
text: `当前对话「${conv.title}」暂时无法移出(无法创建接替对话)`,
|
|
4133
|
+
textEn: `Cannot dismiss the active conversation "${conv.title}" right now (no replacement chat available)`,
|
|
3905
4134
|
});
|
|
4135
|
+
this.emitConversations();
|
|
4136
|
+
this.flushSnapshot();
|
|
4137
|
+
return;
|
|
3906
4138
|
}
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
4139
|
+
}
|
|
4140
|
+
this.removeConversation(id);
|
|
4141
|
+
this.emitConversations();
|
|
4142
|
+
this.flushSnapshot();
|
|
4143
|
+
}
|
|
4144
|
+
/** Move the active marker away from id so that conversation can be removed.
|
|
4145
|
+
* Prefers another listed conversation; falls back to creating a fresh chat.
|
|
4146
|
+
* Returns true when id is no longer active. */
|
|
4147
|
+
async vacateActive(id) {
|
|
4148
|
+
if (id !== this.activeId)
|
|
4149
|
+
return true;
|
|
4150
|
+
const other = [...this.convs.values()].find((c) => c.id !== id && c.listed);
|
|
4151
|
+
if (other) {
|
|
4152
|
+
await this.switchConversation(other.id);
|
|
4153
|
+
}
|
|
4154
|
+
else {
|
|
4155
|
+
await this.newChat();
|
|
4156
|
+
}
|
|
4157
|
+
return this.activeId !== id;
|
|
4158
|
+
}
|
|
4159
|
+
/** 强行关闭:中止自身运行(如在跑)与全部子代理后代(运行中的也停),
|
|
4160
|
+
* 再整体移出;终端/审查/后台唤醒等保留态一并放行。active 的目标先让出
|
|
4161
|
+
* active(vacateActive),active 的后代跳过、让出后再补移。 */
|
|
4162
|
+
async forceDismissConversation(conv, descendants, isStreaming) {
|
|
4163
|
+
const title = conv.title;
|
|
4164
|
+
let stopped = 0;
|
|
4165
|
+
for (const d of descendants) {
|
|
4166
|
+
if (d.id === conv.id)
|
|
4167
|
+
continue;
|
|
4168
|
+
if (this.convs.get(d.id) !== d)
|
|
4169
|
+
continue;
|
|
4170
|
+
if (isStreaming(d)) {
|
|
4171
|
+
try {
|
|
4172
|
+
await this.subagentHost.stopSubagent(d.id);
|
|
4173
|
+
stopped++;
|
|
4174
|
+
}
|
|
4175
|
+
catch {
|
|
4176
|
+
// best effort — removal below disposes the runtime anyway.
|
|
4177
|
+
}
|
|
3914
4178
|
}
|
|
3915
|
-
|
|
4179
|
+
}
|
|
4180
|
+
let selfAborted = false;
|
|
4181
|
+
if (isStreaming(conv)) {
|
|
4182
|
+
selfAborted = true;
|
|
4183
|
+
await this.interruptRun(conv, "已强行关闭");
|
|
4184
|
+
}
|
|
4185
|
+
let removedKids = 0;
|
|
4186
|
+
const deferred = [];
|
|
4187
|
+
for (const d of descendants) {
|
|
4188
|
+
const cur = this.convs.get(d.id);
|
|
4189
|
+
if (!cur || cur.id === conv.id)
|
|
4190
|
+
continue;
|
|
4191
|
+
if (cur.id === this.activeId) {
|
|
4192
|
+
deferred.push(cur);
|
|
4193
|
+
continue;
|
|
4194
|
+
}
|
|
4195
|
+
this.removeConversation(cur.id);
|
|
4196
|
+
removedKids++;
|
|
4197
|
+
}
|
|
4198
|
+
if (conv.id === this.activeId) {
|
|
4199
|
+
const vacated = await this.vacateActive(conv.id);
|
|
4200
|
+
if (!vacated) {
|
|
3916
4201
|
this.emit({
|
|
3917
4202
|
type: "notice",
|
|
3918
4203
|
level: "warning",
|
|
3919
|
-
text: `对话「${
|
|
3920
|
-
textEn: `
|
|
4204
|
+
text: `对话「${title}」暂时无法强行关闭(无法创建接替对话)`,
|
|
4205
|
+
textEn: `Cannot force-dismiss conversation "${title}" right now (no replacement chat available)`,
|
|
3921
4206
|
});
|
|
4207
|
+
this.emitConversations();
|
|
4208
|
+
this.flushSnapshot();
|
|
4209
|
+
return;
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
for (const d of deferred) {
|
|
4213
|
+
if (this.convs.get(d.id) === d && d.id !== this.activeId) {
|
|
4214
|
+
this.removeConversation(d.id);
|
|
4215
|
+
removedKids++;
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
4218
|
+
if (this.convs.get(conv.id) === conv && conv.id !== this.activeId) {
|
|
4219
|
+
this.removeConversation(conv.id);
|
|
4220
|
+
}
|
|
4221
|
+
this.emitConversations();
|
|
4222
|
+
this.flushSnapshot();
|
|
4223
|
+
const remaining = descendants.filter((c) => this.convs.get(c.id) === c).length;
|
|
4224
|
+
this.emit({
|
|
4225
|
+
type: "notice",
|
|
4226
|
+
level: "info",
|
|
4227
|
+
text: `已强行关闭对话「${title}」${removedKids > 0 ? `(含 ${removedKids} 个子代理)` : ""}${selfAborted ? ",本轮运行已中止" : ""}${stopped > 0 ? `,${stopped} 个运行中的子代理已中止` : ""}${remaining > 0 ? `;还有 ${remaining} 个子代理未关闭(已切为当前对话)` : ""}`,
|
|
4228
|
+
textEn: `Force-dismissed conversation "${title}"${removedKids > 0 ? ` (incl. ${removedKids} subagent(s))` : ""}${selfAborted ? ", its run was aborted" : ""}${stopped > 0 ? `, ${stopped} running subagent(s) stopped` : ""}${remaining > 0 ? `; ${remaining} subagent(s) remain (now active)` : ""}`,
|
|
4229
|
+
});
|
|
4230
|
+
}
|
|
4231
|
+
/** Bulk-dismiss finished subagents (left-panel right-click menu).
|
|
4232
|
+
*
|
|
4233
|
+
* parentId omitted = every finished subagent in the running list;
|
|
4234
|
+
* given = the transitive subagent descendants of that conversation
|
|
4235
|
+
* (children, grandchildren, … — parentId chain followed recursively),
|
|
4236
|
+
* plus the conversation itself when IT is a finished subagent.
|
|
4237
|
+
* Finished = idle (not streaming, no retained terminal/review/wake
|
|
4238
|
+
* state). Running ones are skipped, never aborted. Children are removed
|
|
4239
|
+
* before parents so the "parent with live children refuses" guard in
|
|
4240
|
+
* dismissConversation never blocks the batch. The active conversation is
|
|
4241
|
+
* never removed. */
|
|
4242
|
+
async dismissFinishedSubagents(parentId) {
|
|
4243
|
+
const root = parentId?.trim() ? parentId.trim() : undefined;
|
|
4244
|
+
if (root && !this.convs.has(root)) {
|
|
4245
|
+
this.emit({
|
|
4246
|
+
type: "notice",
|
|
4247
|
+
level: "warning",
|
|
4248
|
+
text: "该对话不存在或已关闭",
|
|
4249
|
+
textEn: "This conversation does not exist or is already closed",
|
|
4250
|
+
});
|
|
4251
|
+
return;
|
|
4252
|
+
}
|
|
4253
|
+
// Collect the subtree: every conversation whose parentId chain leads to
|
|
4254
|
+
// root (or every subagent when root is omitted). Child-before-parent
|
|
4255
|
+
// order via depth so parents become dismissable as children leave.
|
|
4256
|
+
const depthOf = (id) => {
|
|
4257
|
+
let d = 0;
|
|
4258
|
+
let cur = this.convs.get(id);
|
|
4259
|
+
const seen = new Set([id]);
|
|
4260
|
+
while (cur?.parentId) {
|
|
4261
|
+
if (seen.has(cur.parentId))
|
|
4262
|
+
break;
|
|
4263
|
+
seen.add(cur.parentId);
|
|
4264
|
+
d++;
|
|
4265
|
+
cur = this.convs.get(cur.parentId);
|
|
4266
|
+
if (!cur)
|
|
4267
|
+
break;
|
|
3922
4268
|
}
|
|
4269
|
+
return d;
|
|
4270
|
+
};
|
|
4271
|
+
const inScope = (conv) => {
|
|
4272
|
+
if (!conv.isSubagent)
|
|
4273
|
+
return false;
|
|
4274
|
+
if (conv.id === this.activeId)
|
|
4275
|
+
return false;
|
|
4276
|
+
if (!conv.listed)
|
|
4277
|
+
return false;
|
|
4278
|
+
if (!root)
|
|
4279
|
+
return true;
|
|
4280
|
+
if (conv.id === root)
|
|
4281
|
+
return true;
|
|
4282
|
+
let cur = conv;
|
|
4283
|
+
const seen = new Set();
|
|
4284
|
+
while (cur?.parentId) {
|
|
4285
|
+
if (cur.parentId === root)
|
|
4286
|
+
return true;
|
|
4287
|
+
if (seen.has(cur.parentId))
|
|
4288
|
+
return false;
|
|
4289
|
+
seen.add(cur.parentId);
|
|
4290
|
+
cur = this.convs.get(cur.parentId);
|
|
4291
|
+
if (!cur)
|
|
4292
|
+
return false;
|
|
4293
|
+
}
|
|
4294
|
+
return false;
|
|
4295
|
+
};
|
|
4296
|
+
const isStreaming = (conv) => {
|
|
4297
|
+
try {
|
|
4298
|
+
return conv.session.isStreaming;
|
|
4299
|
+
}
|
|
4300
|
+
catch {
|
|
4301
|
+
return true;
|
|
4302
|
+
}
|
|
4303
|
+
};
|
|
4304
|
+
const candidates = [...this.convs.values()]
|
|
4305
|
+
.filter(inScope)
|
|
4306
|
+
// Running first would be pointless — drop streaming/retained up front.
|
|
4307
|
+
.filter((conv) => !isStreaming(conv))
|
|
4308
|
+
.filter((conv) => !shouldRetainActive({
|
|
4309
|
+
reviewing: conv.goal.reviewing,
|
|
4310
|
+
wizardRunning: conv.wizardRunning,
|
|
4311
|
+
streaming: false,
|
|
4312
|
+
openTerminals: conv.terminals.countLive(),
|
|
4313
|
+
listed: false,
|
|
4314
|
+
promptedSinceActive: false,
|
|
4315
|
+
hasActiveSubagentRun: () => hasActiveSubagentRun({ sessionId: conv.session.sessionFile }),
|
|
4316
|
+
hasPendingWake: () => hasPendingWaitSubscription({ sessionId: conv.session.sessionFile }),
|
|
4317
|
+
}))
|
|
4318
|
+
.sort((a, b) => depthOf(b.id) - depthOf(a.id));
|
|
4319
|
+
if (candidates.length === 0) {
|
|
4320
|
+
this.emit({
|
|
4321
|
+
type: "notice",
|
|
4322
|
+
level: "info",
|
|
4323
|
+
text: "没有可关闭的已结束子代理",
|
|
4324
|
+
textEn: "No finished subagents to dismiss",
|
|
4325
|
+
});
|
|
3923
4326
|
return;
|
|
3924
4327
|
}
|
|
3925
|
-
|
|
4328
|
+
let removed = 0;
|
|
4329
|
+
let skippedRunning = 0;
|
|
4330
|
+
for (const conv of candidates) {
|
|
4331
|
+
const cur = this.convs.get(conv.id);
|
|
4332
|
+
if (!cur || cur.id === this.activeId)
|
|
4333
|
+
continue;
|
|
4334
|
+
if (isStreaming(cur)) {
|
|
4335
|
+
skippedRunning++;
|
|
4336
|
+
continue;
|
|
4337
|
+
}
|
|
4338
|
+
// Re-check live children: earlier removals in this same batch may
|
|
4339
|
+
// have cleared the guard; still-running children block the parent.
|
|
4340
|
+
const liveChild = [...this.convs.values()].some((child) => child.parentId === cur.id && isStreaming(child));
|
|
4341
|
+
if (liveChild) {
|
|
4342
|
+
skippedRunning++;
|
|
4343
|
+
continue;
|
|
4344
|
+
}
|
|
4345
|
+
this.removeConversation(cur.id);
|
|
4346
|
+
removed++;
|
|
4347
|
+
}
|
|
3926
4348
|
this.emitConversations();
|
|
3927
4349
|
this.flushSnapshot();
|
|
4350
|
+
if (removed > 0) {
|
|
4351
|
+
this.emit({
|
|
4352
|
+
type: "notice",
|
|
4353
|
+
level: "info",
|
|
4354
|
+
text: `已关闭 ${removed} 个已结束的子代理${skippedRunning > 0 ? `(${skippedRunning} 个仍在运行,已跳过)` : ""}`,
|
|
4355
|
+
textEn: `Dismissed ${removed} finished subagent(s)${skippedRunning > 0 ? ` (${skippedRunning} still running, skipped)` : ""}`,
|
|
4356
|
+
});
|
|
4357
|
+
}
|
|
4358
|
+
else {
|
|
4359
|
+
this.emit({
|
|
4360
|
+
type: "notice",
|
|
4361
|
+
level: "info",
|
|
4362
|
+
text: "没有可关闭的已结束子代理(剩余的仍在运行)",
|
|
4363
|
+
textEn: "No finished subagents to dismiss (the rest are still running)",
|
|
4364
|
+
});
|
|
4365
|
+
}
|
|
3928
4366
|
}
|
|
3929
4367
|
/** Open a persisted session as the active conversation (from listSessions).
|
|
3930
4368
|
*
|
|
@@ -3963,7 +4401,9 @@ export class ClientSession {
|
|
|
3963
4401
|
// is known-good. This keeps a failed history open entirely non-destructive.
|
|
3964
4402
|
const oldListed = this.conv.listed;
|
|
3965
4403
|
const displaced = this.displaceActive();
|
|
3966
|
-
const openInProject = [...this.convs.values()].filter((c) => c.cwd === targetCwd).length +
|
|
4404
|
+
const openInProject = [...this.convs.values()].filter((c) => c.cwd === targetCwd && !c.isSubagent).length +
|
|
4405
|
+
1 -
|
|
4406
|
+
(displaced?.cwd === targetCwd && !displaced?.isSubagent ? 1 : 0);
|
|
3967
4407
|
if (openInProject > MAX_OPEN_CONVERSATIONS) {
|
|
3968
4408
|
// displaceActive() may have promoted a streaming conversation into the
|
|
3969
4409
|
// running list. Roll that presentation-only mutation back because no
|
|
@@ -4367,14 +4807,29 @@ export class ClientSession {
|
|
|
4367
4807
|
}
|
|
4368
4808
|
this.flushSnapshot();
|
|
4369
4809
|
}
|
|
4810
|
+
/** Strip the "(New)" freshness marker some catalogs append to display names
|
|
4811
|
+
* (pi.dev data, e.g. "DeepSeek V4 Pro (New)") — display-only; the model id
|
|
4812
|
+
* is untouched so switching still uses the exact official id. */
|
|
4813
|
+
cleanModelDisplayName(name) {
|
|
4814
|
+
return name.replace(/\s*\(new\)$/i, "").trim();
|
|
4815
|
+
}
|
|
4370
4816
|
/** List models that have valid authentication configured. */
|
|
4371
4817
|
async listModels() {
|
|
4372
4818
|
try {
|
|
4373
4819
|
const mr = this.runtime.services.modelRuntime;
|
|
4820
|
+
// Reconcile built-in provider catalogs with the official pi.dev
|
|
4821
|
+
// endpoint before listing: within the SDK's 4h freshness window this
|
|
4822
|
+
// is a fast 304; past it the newest catalog is downloaded WHOLESALE
|
|
4823
|
+
// (patch-remote-catalog.ts) — no union merge, no stale built-in
|
|
4824
|
+
// leftovers, no "新增 N 个模型" noise. Network failure falls back to
|
|
4825
|
+
// the cached catalog silently.
|
|
4826
|
+
await mr.refresh({ allowNetwork: true, signal: AbortSignal.timeout(15_000) }).catch(() => {
|
|
4827
|
+
// list must never fail because the catalog sync did
|
|
4828
|
+
});
|
|
4374
4829
|
const available = await mr.getAvailable();
|
|
4375
4830
|
const models = available.map((m) => ({
|
|
4376
4831
|
id: `${m.provider}/${m.id}`,
|
|
4377
|
-
name: m.name,
|
|
4832
|
+
name: this.cleanModelDisplayName(m.name),
|
|
4378
4833
|
provider: m.provider,
|
|
4379
4834
|
reasoning: m.reasoning,
|
|
4380
4835
|
vision: m.input?.includes("image") ?? false,
|