pi-web-ui 0.60.0 → 0.62.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/LICENSE +21 -21
- package/README.md +497 -497
- package/README.zh-CN.md +422 -422
- package/bin/pi-web-ui.mjs +1881 -1881
- package/deploy/com.xingshuyin.pi-web-ui.plist +48 -48
- package/deploy/nginx-subpath.conf +88 -88
- package/deploy/pi-web-ui-task.xml +54 -54
- package/deploy/pi-web-ui.service +31 -31
- package/dist/server/agent-service.js +207 -9
- package/dist/server/attachments.js +3 -3
- package/dist/server/client-state.js +52 -0
- package/dist/server/dsh/dsh-agent-service.js +29 -1
- package/dist/server/dsh/runtime/goal-rpc.mjs +662 -662
- package/dist/server/index.js +16 -1
- package/dist/server/model-admin.js +360 -35
- package/dist/server/terminals.js +4 -4
- package/extensions/webui.ts +192 -192
- package/package.json +103 -103
- package/web/dist/assets/TerminalPanel-BkCWDsIG.js +2 -0
- package/web/dist/assets/index-BRVKed7W.js +321 -0
- package/web/dist/assets/index-ORTpR6ZQ.css +10 -0
- package/web/dist/favicon.svg +8 -8
- package/web/dist/index.html +15 -15
- package/web/public/favicon.svg +8 -8
- package/web/dist/assets/TerminalPanel-CSzqnWV2.js +0 -2
- package/web/dist/assets/index-D9iD9G_p.css +0 -10
- package/web/dist/assets/index-TZDcJFdi.js +0 -321
|
@@ -82,14 +82,14 @@ export class QuiesceRejectedError extends Error {
|
|
|
82
82
|
* programs wait for input that never comes. Legacy Chinese files are often
|
|
83
83
|
* GBK/GB2312 — read them with the right encoding, never paste mojibake into
|
|
84
84
|
* reasoning/answers. */
|
|
85
|
-
const WINDOWS_PERSONA = `You are a coding agent running on Windows. The bash tool runs Git Bash (bash.exe), not PowerShell. Follow these rules to avoid hanging the session:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
- ALWAYS pass a timeout parameter to the bash tool (in seconds). There is NO default timeout — a command that never finishes (servers, watchers, infinite loops, slow downloads/installs) will hang the entire conversation indefinitely. Pick a generous timeout for long-running work, but never omit it.
|
|
90
|
-
- NEVER run interactive or foreground long-running commands through the bash tool (vi, less, top, python -, node -, npm run dev, sleep 10000). For servers/daemons use background execution with output redirected to a log file, then poll the log; stop them when done.
|
|
91
|
-
- In the interactive terminal (TTY) — which is Git Bash too, not PowerShell — NEVER use heredocs (<<'EOF' ... EOF) or here-strings, and NEVER start interactive programs (vi, less, python -, node -, npm init): they wait for keyboard input that never arrives and hang the terminal forever. Prefer writing a temp script file (e.g. .pi-tmp.sh) and running it non-interactively. ALWAYS pass a timeout to long-running commands (e.g. \`timeout 120 npm run dev\`).
|
|
92
|
-
|
|
85
|
+
const WINDOWS_PERSONA = `You are a coding agent running on Windows. The bash tool runs Git Bash (bash.exe), not PowerShell. Follow these rules to avoid hanging the session:
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
- ALWAYS pass a timeout parameter to the bash tool (in seconds). There is NO default timeout — a command that never finishes (servers, watchers, infinite loops, slow downloads/installs) will hang the entire conversation indefinitely. Pick a generous timeout for long-running work, but never omit it.
|
|
90
|
+
- NEVER run interactive or foreground long-running commands through the bash tool (vi, less, top, python -, node -, npm run dev, sleep 10000). For servers/daemons use background execution with output redirected to a log file, then poll the log; stop them when done.
|
|
91
|
+
- In the interactive terminal (TTY) — which is Git Bash too, not PowerShell — NEVER use heredocs (<<'EOF' ... EOF) or here-strings, and NEVER start interactive programs (vi, less, python -, node -, npm init): they wait for keyboard input that never arrives and hang the terminal forever. Prefer writing a temp script file (e.g. .pi-tmp.sh) and running it non-interactively. ALWAYS pass a timeout to long-running commands (e.g. \`timeout 120 npm run dev\`).
|
|
92
|
+
|
|
93
93
|
Many legacy Chinese text files (.html/.txt/.md/.log, exported documents) are GBK/GB2312 encoded: the read tool decodes UTF-8 only and will show mojibake (乱码) for them. If a file's content looks garbled, read it through the terminal instead: in Git Bash use \`cat file | iconv -f GBK -t UTF-8\` (or \`iconv -f GBK -t UTF-8 file\`); in cmd use \`chcp 65001 && type file\`; in PowerShell use \`Get-Content -Encoding Default file\`. Never paste mojibake into your reasoning or answer — describe the decoded content instead.`;
|
|
94
94
|
/** bash 工具输出限制/过滤管道引导:模型习惯套 `| tail/-n`、`| head`、`| grep`、`| less`
|
|
95
95
|
* 等限输出。这些管道在持久终端里会①缓冲(可见终端全程哑火、看不到实时进度)②把退出码
|
|
@@ -733,6 +733,8 @@ export class ClientSession {
|
|
|
733
733
|
}
|
|
734
734
|
}
|
|
735
735
|
await cs.bindSession();
|
|
736
|
+
await cs.restoreProjectProviderKeysForCwd(cwd);
|
|
737
|
+
await cs.restoreProjectModelForCwd(cwd);
|
|
736
738
|
return cs;
|
|
737
739
|
}
|
|
738
740
|
/**
|
|
@@ -928,6 +930,9 @@ export class ClientSession {
|
|
|
928
930
|
// Reconnect: push the background-task list — it must survive reconnects
|
|
929
931
|
// and outlive the conversation that started the tasks.
|
|
930
932
|
this.bg.push();
|
|
933
|
+
// Reconnect: push the built-in provider key list (multi-key grouping in the
|
|
934
|
+
// model picker needs it even before the client asks).
|
|
935
|
+
this.modelAdmin.listProviderKeys();
|
|
931
936
|
// PTYs are conversation-owned and survive a socket reconnect.
|
|
932
937
|
this.pushTerminals();
|
|
933
938
|
}
|
|
@@ -1813,6 +1818,120 @@ export class ClientSession {
|
|
|
1813
1818
|
deleteModelConfig(providerId) {
|
|
1814
1819
|
return this.modelAdmin.deleteModelConfig(providerId);
|
|
1815
1820
|
}
|
|
1821
|
+
listProviderKeys() {
|
|
1822
|
+
return this.modelAdmin.listProviderKeys();
|
|
1823
|
+
}
|
|
1824
|
+
async addProviderKey(provider, apiKey, name) {
|
|
1825
|
+
await this.modelAdmin.addProviderKey(provider, apiKey, name);
|
|
1826
|
+
const active = this.modelAdmin.getActiveKeyName(provider);
|
|
1827
|
+
if (active)
|
|
1828
|
+
this.stateStore.saveProjectProviderKey(this.clientId, this.cwd, provider, active);
|
|
1829
|
+
}
|
|
1830
|
+
async activateProviderKey(provider, keyName) {
|
|
1831
|
+
await this.modelAdmin.activateProviderKey(provider, keyName);
|
|
1832
|
+
this.stateStore.saveProjectProviderKey(this.clientId, this.cwd, provider, keyName);
|
|
1833
|
+
}
|
|
1834
|
+
async removeProviderKey(provider, keyName) {
|
|
1835
|
+
await this.modelAdmin.removeProviderKey(provider, keyName);
|
|
1836
|
+
const saved = this.stateStore.getProjectProviderKey(this.clientId, this.cwd, provider);
|
|
1837
|
+
if (saved === keyName) {
|
|
1838
|
+
const active = this.modelAdmin.getActiveKeyName(provider);
|
|
1839
|
+
if (active)
|
|
1840
|
+
this.stateStore.saveProjectProviderKey(this.clientId, this.cwd, provider, active);
|
|
1841
|
+
else
|
|
1842
|
+
this.stateStore.deleteProjectProviderKey(this.clientId, this.cwd, provider);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
/** Restore per-project provider keys when entering a project. For each
|
|
1846
|
+
* provider that has a saved key for `cwd`, activate it if it differs from
|
|
1847
|
+
* the current global active. Silent — no notice spam on project switch. */
|
|
1848
|
+
async restoreProjectProviderKeysForCwd(cwd) {
|
|
1849
|
+
const saved = this.stateStore.getProjectProviderKeys(this.clientId, cwd);
|
|
1850
|
+
if (!saved)
|
|
1851
|
+
return;
|
|
1852
|
+
for (const [provider, keyName] of Object.entries(saved)) {
|
|
1853
|
+
const cur = this.modelAdmin.getActiveKeyName(provider);
|
|
1854
|
+
if (cur === keyName)
|
|
1855
|
+
continue;
|
|
1856
|
+
try {
|
|
1857
|
+
await this.modelAdmin.activateProviderKey(provider, keyName);
|
|
1858
|
+
}
|
|
1859
|
+
catch {
|
|
1860
|
+
// saved key may have been deleted — ignore
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
/** When a model is set, ensure its provider's per-project key is restored. */
|
|
1865
|
+
async restoreKeyForModel(modelId, cwd) {
|
|
1866
|
+
const slash = modelId.indexOf("/");
|
|
1867
|
+
if (slash <= 0)
|
|
1868
|
+
return;
|
|
1869
|
+
const provider = modelId.slice(0, slash);
|
|
1870
|
+
const saved = this.stateStore.getProjectProviderKey(this.clientId, cwd, provider);
|
|
1871
|
+
if (!saved)
|
|
1872
|
+
return;
|
|
1873
|
+
const cur = this.modelAdmin.getActiveKeyName(provider);
|
|
1874
|
+
if (cur === saved)
|
|
1875
|
+
return;
|
|
1876
|
+
try {
|
|
1877
|
+
await this.modelAdmin.activateProviderKey(provider, saved);
|
|
1878
|
+
}
|
|
1879
|
+
catch { }
|
|
1880
|
+
}
|
|
1881
|
+
/** Remember the just-selected model (and the key that was active for its
|
|
1882
|
+
* provider) for the current project. Called IMMEDIATELY on model selection —
|
|
1883
|
+
* not only after a turn — so switching back to the project restores the exact
|
|
1884
|
+
* {model, key} left behind, even for a fresh conversation with no assistant
|
|
1885
|
+
* message yet (the SDK only flushes a model_change to disk once one exists). */
|
|
1886
|
+
rememberProjectModel(modelId) {
|
|
1887
|
+
const cwd = this.cwd;
|
|
1888
|
+
this.stateStore.saveProjectModel(this.clientId, cwd, modelId);
|
|
1889
|
+
const slash = modelId.indexOf("/");
|
|
1890
|
+
if (slash <= 0)
|
|
1891
|
+
return;
|
|
1892
|
+
const provider = modelId.slice(0, slash);
|
|
1893
|
+
const active = this.modelAdmin.getActiveKeyName(provider);
|
|
1894
|
+
if (active)
|
|
1895
|
+
this.stateStore.saveProjectProviderKey(this.clientId, cwd, provider, active);
|
|
1896
|
+
}
|
|
1897
|
+
/** Restore the project's remembered model (and its provider's key) onto the
|
|
1898
|
+
* ACTIVE conversation — but ONLY for a conversation the user hasn't really
|
|
1899
|
+
* started (no messages yet). A conversation that already has content keeps its
|
|
1900
|
+
* own per-session model: switching back to a RUNNING / completed chat must not
|
|
1901
|
+
* silently overwrite its model with the project default. So a fresh chat in the
|
|
1902
|
+
* project gets the remembered model; an in-progress one keeps what it had and
|
|
1903
|
+
* the user switches via the picker. Silent on failure (model no longer in catalog). */
|
|
1904
|
+
async restoreProjectModelForCwd(cwd) {
|
|
1905
|
+
const savedModel = this.stateStore.getProjectModel(this.clientId, cwd);
|
|
1906
|
+
if (!savedModel)
|
|
1907
|
+
return;
|
|
1908
|
+
try {
|
|
1909
|
+
if (this.conv.session.getSessionStats().totalMessages > 0)
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
catch {
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
try {
|
|
1916
|
+
const mr = this.runtime.services.modelRuntime;
|
|
1917
|
+
const slash = savedModel.indexOf("/");
|
|
1918
|
+
if (slash <= 0 || slash === savedModel.length - 1)
|
|
1919
|
+
return;
|
|
1920
|
+
const model = mr.getModel(savedModel.slice(0, slash), savedModel.slice(slash + 1));
|
|
1921
|
+
if (!model)
|
|
1922
|
+
return;
|
|
1923
|
+
const cur = this.session.model;
|
|
1924
|
+
const curId = cur ? `${cur.provider}/${cur.id}` : null;
|
|
1925
|
+
// Restore the model's provider key first so setModel's auth check passes.
|
|
1926
|
+
await this.restoreKeyForModel(savedModel, cwd);
|
|
1927
|
+
if (curId === savedModel)
|
|
1928
|
+
return;
|
|
1929
|
+
await this.session.setModel(model);
|
|
1930
|
+
}
|
|
1931
|
+
catch {
|
|
1932
|
+
// model no longer resolvable / key gone — keep the conversation default
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1816
1935
|
// ---------------------------------------------------------------------------
|
|
1817
1936
|
// Settings (system prompt / skills / extensions / presets)
|
|
1818
1937
|
// ---------------------------------------------------------------------------
|
|
@@ -2047,6 +2166,63 @@ export class ClientSession {
|
|
|
2047
2166
|
await this.interruptRun(this.conv, "已停止");
|
|
2048
2167
|
this.flushSnapshot();
|
|
2049
2168
|
}
|
|
2169
|
+
/**
|
|
2170
|
+
* Remove ONE queued prompt text (the ✕ on a pending bubble) so it is neither
|
|
2171
|
+
* shown nor eventually delivered. The pi SDK has no per-item queue API, so we
|
|
2172
|
+
* drain the SDK queue (clearQueue), drop the target text and re-queue the rest
|
|
2173
|
+
* in their original order; the SDK re-emits queue_update which re-syncs
|
|
2174
|
+
* conv.queueSteering / conv.queueFollowUp.
|
|
2175
|
+
*/
|
|
2176
|
+
async removeQueued(kind, text) {
|
|
2177
|
+
const conv = this.conv;
|
|
2178
|
+
// Always-defined display mirrors; also the provenance of bubble rendering.
|
|
2179
|
+
const local = kind === "steer" ? conv.queueSteering : conv.queueFollowUp;
|
|
2180
|
+
if (!local.includes(text)) {
|
|
2181
|
+
// Already gone (delivered / cleared elsewhere) — just refresh the display.
|
|
2182
|
+
this.flushSnapshot();
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
const s = this.conv.session;
|
|
2186
|
+
if (!s) {
|
|
2187
|
+
// Runtime not bound yet (fresh conversation) — drop the display mirror;
|
|
2188
|
+
// a later queue_update reconciles any SDK-side state.
|
|
2189
|
+
const i = local.indexOf(text);
|
|
2190
|
+
if (i >= 0)
|
|
2191
|
+
local.splice(i, 1);
|
|
2192
|
+
this.flushSnapshot();
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
const { steering, followUp } = s.clearQueue();
|
|
2196
|
+
const keptSteering = steering.filter((t) => !(kind === "steer" && t === text));
|
|
2197
|
+
const keptFollowUp = followUp.filter((t) => !(kind === "followUp" && t === text));
|
|
2198
|
+
// Re-queue the survivors in original order. Guard each call so a single
|
|
2199
|
+
// failure can't leave the queue half-drained silently.
|
|
2200
|
+
for (const t of keptSteering) {
|
|
2201
|
+
try {
|
|
2202
|
+
await s.steer(t);
|
|
2203
|
+
}
|
|
2204
|
+
catch (err) {
|
|
2205
|
+
this.emit({
|
|
2206
|
+
type: "notice",
|
|
2207
|
+
level: "error",
|
|
2208
|
+
text: `重新入队插队消息失败:${err.message}`,
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
for (const t of keptFollowUp) {
|
|
2213
|
+
try {
|
|
2214
|
+
await s.followUp(t);
|
|
2215
|
+
}
|
|
2216
|
+
catch (err) {
|
|
2217
|
+
this.emit({
|
|
2218
|
+
type: "notice",
|
|
2219
|
+
level: "error",
|
|
2220
|
+
text: `重新入队排队消息失败:${err.message}`,
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
this.flushSnapshot();
|
|
2225
|
+
}
|
|
2050
2226
|
/** Re-push the current list on request (panel opened); prunes dead entries first. */
|
|
2051
2227
|
async listBgServers() {
|
|
2052
2228
|
await this.bg.listAndPush();
|
|
@@ -2266,6 +2442,9 @@ export class ClientSession {
|
|
|
2266
2442
|
if (prevModel && this.sharedModelRuntime) {
|
|
2267
2443
|
try {
|
|
2268
2444
|
await this.session.setModel(prevModel);
|
|
2445
|
+
const p = prevModel.provider;
|
|
2446
|
+
const mid = `${p}/${prevModel.id}`;
|
|
2447
|
+
await this.restoreKeyForModel(mid, this.cwd);
|
|
2269
2448
|
}
|
|
2270
2449
|
catch {
|
|
2271
2450
|
// model no longer resolvable — keep the default
|
|
@@ -2363,6 +2542,8 @@ export class ClientSession {
|
|
|
2363
2542
|
void this.pushSlashCommands();
|
|
2364
2543
|
if (cwdChanged) {
|
|
2365
2544
|
this.cwd = newCwd;
|
|
2545
|
+
await this.restoreProjectProviderKeysForCwd(newCwd);
|
|
2546
|
+
await this.restoreProjectModelForCwd(newCwd);
|
|
2366
2547
|
// Mirror set_cwd's project-switch side-effects so the whole UI follows
|
|
2367
2548
|
// the new workspace, not just the chat pane.
|
|
2368
2549
|
try {
|
|
@@ -2645,6 +2826,8 @@ export class ClientSession {
|
|
|
2645
2826
|
this.removeConversation(displaced.id);
|
|
2646
2827
|
await this.bindSession();
|
|
2647
2828
|
this.cwd = targetCwd;
|
|
2829
|
+
await this.restoreProjectProviderKeysForCwd(targetCwd);
|
|
2830
|
+
await this.restoreProjectModelForCwd(targetCwd);
|
|
2648
2831
|
this.conv.lastActiveAt = Date.now();
|
|
2649
2832
|
this.webUi.refresh();
|
|
2650
2833
|
this.emitConversations();
|
|
@@ -2746,6 +2929,8 @@ export class ClientSession {
|
|
|
2746
2929
|
if (prevModel && this.sharedModelRuntime) {
|
|
2747
2930
|
try {
|
|
2748
2931
|
await this.session.setModel(prevModel);
|
|
2932
|
+
const pm = prevModel;
|
|
2933
|
+
await this.restoreKeyForModel(`${pm.provider}/${pm.id}`, this.cwd);
|
|
2749
2934
|
}
|
|
2750
2935
|
catch {
|
|
2751
2936
|
// model no longer resolvable — keep the default
|
|
@@ -2861,7 +3046,13 @@ export class ClientSession {
|
|
|
2861
3046
|
}
|
|
2862
3047
|
async cycleModel() {
|
|
2863
3048
|
try {
|
|
2864
|
-
await this.session.cycleModel();
|
|
3049
|
+
const result = await this.session.cycleModel();
|
|
3050
|
+
if (result?.model) {
|
|
3051
|
+
const mid = `${result.model.provider}/${result.model.id}`;
|
|
3052
|
+
await this.restoreKeyForModel(mid, this.cwd);
|
|
3053
|
+
// Remember per-project like setModel — cycling is also a model switch.
|
|
3054
|
+
this.rememberProjectModel(mid);
|
|
3055
|
+
}
|
|
2865
3056
|
}
|
|
2866
3057
|
catch (err) {
|
|
2867
3058
|
this.emit({
|
|
@@ -2942,6 +3133,8 @@ export class ClientSession {
|
|
|
2942
3133
|
this.conv.promptedSinceActive = false;
|
|
2943
3134
|
this.conv.lastActiveAt = Date.now();
|
|
2944
3135
|
this.cwd = abs;
|
|
3136
|
+
await this.restoreProjectProviderKeysForCwd(abs);
|
|
3137
|
+
await this.restoreProjectModelForCwd(abs);
|
|
2945
3138
|
// 工作区跟随型插件(编辑器文件树等)同步切根。
|
|
2946
3139
|
try {
|
|
2947
3140
|
this.onCwdChanged?.(abs);
|
|
@@ -3041,6 +3234,11 @@ export class ClientSession {
|
|
|
3041
3234
|
if (!model)
|
|
3042
3235
|
throw new Error(`模型不存在:${modelId}`);
|
|
3043
3236
|
await this.session.setModel(model);
|
|
3237
|
+
await this.restoreKeyForModel(modelId, this.cwd);
|
|
3238
|
+
// Immediately remember the model + the key it uses for the current
|
|
3239
|
+
// project (not only after a turn). This is what makes project switching
|
|
3240
|
+
// restore both the model and the provider key.
|
|
3241
|
+
this.rememberProjectModel(modelId);
|
|
3044
3242
|
}
|
|
3045
3243
|
catch (err) {
|
|
3046
3244
|
this.emit({
|
|
@@ -448,9 +448,9 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
448
448
|
content: [
|
|
449
449
|
{
|
|
450
450
|
type: "text",
|
|
451
|
-
text: `
|
|
452
|
-
<vision-bridge>
|
|
453
|
-
${transcript}
|
|
451
|
+
text: `
|
|
452
|
+
<vision-bridge>
|
|
453
|
+
${transcript}
|
|
454
454
|
</vision-bridge>`,
|
|
455
455
|
},
|
|
456
456
|
...(pathImg
|
|
@@ -231,4 +231,56 @@ export class ClientStateStore {
|
|
|
231
231
|
state.presets = presets;
|
|
232
232
|
this.save();
|
|
233
233
|
}
|
|
234
|
+
/** Get per-project provider keys for a cwd, or undefined. */
|
|
235
|
+
getProjectProviderKeys(clientId, cwd) {
|
|
236
|
+
return this.load()[clientId]?.projectProviderKeys?.[cwd];
|
|
237
|
+
}
|
|
238
|
+
/** Get a single provider's saved key for a project. */
|
|
239
|
+
getProjectProviderKey(clientId, cwd, provider) {
|
|
240
|
+
return this.load()[clientId]?.projectProviderKeys?.[cwd]?.[provider];
|
|
241
|
+
}
|
|
242
|
+
/** Remember which key was last used for a provider in a project. */
|
|
243
|
+
saveProjectProviderKey(clientId, cwd, provider, keyName) {
|
|
244
|
+
const all = this.load();
|
|
245
|
+
const state = (all[clientId] ??= { projects: [] });
|
|
246
|
+
const map = (state.projectProviderKeys ??= {});
|
|
247
|
+
const inner = (map[cwd] ??= {});
|
|
248
|
+
inner[provider] = keyName;
|
|
249
|
+
this.save();
|
|
250
|
+
}
|
|
251
|
+
/** Delete a per-project provider key (e.g. when the key is removed). */
|
|
252
|
+
deleteProjectProviderKey(clientId, cwd, provider) {
|
|
253
|
+
const all = this.load();
|
|
254
|
+
const inner = all[clientId]?.projectProviderKeys?.[cwd];
|
|
255
|
+
if (!inner || !(provider in inner))
|
|
256
|
+
return;
|
|
257
|
+
delete inner[provider];
|
|
258
|
+
if (Object.keys(inner).length === 0) {
|
|
259
|
+
delete all[clientId].projectProviderKeys[cwd];
|
|
260
|
+
}
|
|
261
|
+
this.save();
|
|
262
|
+
}
|
|
263
|
+
/** Get the model the user last selected in a project, or undefined. */
|
|
264
|
+
getProjectModel(clientId, cwd) {
|
|
265
|
+
return this.load()[clientId]?.projectModels?.[cwd];
|
|
266
|
+
}
|
|
267
|
+
/** Remember the model last selected in a project (immediate, not after a turn). */
|
|
268
|
+
saveProjectModel(clientId, cwd, modelId) {
|
|
269
|
+
const all = this.load();
|
|
270
|
+
const state = (all[clientId] ??= { projects: [] });
|
|
271
|
+
(state.projectModels ??= {})[cwd] = modelId;
|
|
272
|
+
this.save();
|
|
273
|
+
}
|
|
274
|
+
/** Drop the per-project model memory for a project (e.g. when the model is
|
|
275
|
+
* removed from the catalog). */
|
|
276
|
+
deleteProjectModel(clientId, cwd) {
|
|
277
|
+
const all = this.load();
|
|
278
|
+
const map = all[clientId]?.projectModels;
|
|
279
|
+
if (!map || !(cwd in map))
|
|
280
|
+
return;
|
|
281
|
+
delete map[cwd];
|
|
282
|
+
if (Object.keys(map).length === 0)
|
|
283
|
+
delete all[clientId].projectModels;
|
|
284
|
+
this.save();
|
|
285
|
+
}
|
|
234
286
|
}
|
|
@@ -1157,6 +1157,20 @@ export class DshClientSession {
|
|
|
1157
1157
|
}
|
|
1158
1158
|
await this.promptConv(conv, text, attachments, queue);
|
|
1159
1159
|
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Remove ONE queued prompt text (the ✕ on a pending bubble). DSH queues are
|
|
1162
|
+
* display-only — the prompt was already handed to the runtime inbox, so this
|
|
1163
|
+
* only drops the pending bubble from the UI (there is no per-item runtime
|
|
1164
|
+
* cancel). Removes the first occurrence of `text`.
|
|
1165
|
+
*/
|
|
1166
|
+
removeQueued(kind, text) {
|
|
1167
|
+
const conv = this.conv;
|
|
1168
|
+
const arr = kind === "steer" ? conv.queue.steering : conv.queue.followUp;
|
|
1169
|
+
const i = arr.indexOf(text);
|
|
1170
|
+
if (i >= 0)
|
|
1171
|
+
arr.splice(i, 1);
|
|
1172
|
+
this.flushSnapshot();
|
|
1173
|
+
}
|
|
1160
1174
|
/** 向指定会话发提示(审查注入/重发用;不处理 fromDisk fork 与命名)。 */
|
|
1161
1175
|
/** 排空期拒绝新工作(与 pi 引擎同文案;存量运行继续)。 */
|
|
1162
1176
|
quiesceBlocked() {
|
|
@@ -2526,6 +2540,18 @@ export class DshClientSession {
|
|
|
2526
2540
|
],
|
|
2527
2541
|
});
|
|
2528
2542
|
}
|
|
2543
|
+
listProviderKeys() {
|
|
2544
|
+
this.emit({ type: "provider_keys", keys: {} });
|
|
2545
|
+
}
|
|
2546
|
+
async addProviderKey(provider, apiKey, name) {
|
|
2547
|
+
this.emit({ type: "notice", level: "warning", text: "DSH 引擎不支持多密钥" });
|
|
2548
|
+
}
|
|
2549
|
+
async activateProviderKey(provider, keyName) {
|
|
2550
|
+
this.emit({ type: "notice", level: "warning", text: "DSH 引擎不支持多密钥" });
|
|
2551
|
+
}
|
|
2552
|
+
async removeProviderKey(provider, keyName) {
|
|
2553
|
+
this.emit({ type: "notice", level: "warning", text: "DSH 引擎不支持多密钥" });
|
|
2554
|
+
}
|
|
2529
2555
|
async fetchModelsList(reqId, baseUrl, apiKey, authHeader, api) {
|
|
2530
2556
|
this.emit({ type: "fetch_models_result", reqId, ok: false, error: "DSH 引擎不支持自定义 provider 探测" });
|
|
2531
2557
|
}
|
|
@@ -2533,7 +2559,9 @@ export class DshClientSession {
|
|
|
2533
2559
|
this.emit({ type: "refresh_provider_result", reqId, ok: false, error: "DSH 引擎不支持自定义 provider" });
|
|
2534
2560
|
}
|
|
2535
2561
|
async cloneProvider(provider, reqId) {
|
|
2536
|
-
|
|
2562
|
+
const error = "DSH 引擎不支持自定义 provider";
|
|
2563
|
+
this.emit({ type: "notice", level: "error", text: error });
|
|
2564
|
+
this.emit({ type: "clone_provider_result", reqId, ok: false, error });
|
|
2537
2565
|
}
|
|
2538
2566
|
// -----------------------------------------------------------------------
|
|
2539
2567
|
// 其他
|