pi-web-ui 0.86.2 → 0.87.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 +59 -21
- package/README.md +1 -1
- package/bin/pi-web-ui.mjs +312 -40
- package/dist/server/agent-service.js +297 -6
- package/dist/server/client-state.js +8 -0
- package/dist/server/composer-drafts.js +138 -0
- package/dist/server/dsh/dsh-agent-service.js +531 -54
- package/dist/server/dsh/dsh-client.js +28 -0
- package/dist/server/dsh/dsh-sessions.js +28 -0
- package/dist/server/dsh/dsh-usage.js +82 -0
- package/dist/server/dsh/preset-clones.js +260 -0
- package/dist/server/dsh/runtime/custom-prompt.mjs +33 -0
- package/dist/server/dsh/runtime/goal-rpc.mjs +406 -22
- package/dist/server/dsh/runtime/launcher.mjs +17 -0
- package/dist/server/dsh/runtime/override.patch.yml +19 -1
- package/dist/server/files-service.js +259 -1
- package/dist/server/index.js +219 -3
- package/dist/server/mcp-bridge.js +126 -22
- package/dist/server/mcp-hot-reload.js +117 -0
- package/dist/server/model-admin.js +93 -3
- package/dist/server/plugin-dom.js +83 -0
- package/dist/server/plugin-facilities.js +15 -1
- package/dist/server/plugin-installer.js +6 -0
- package/dist/server/plugins.js +781 -74
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/provider-oauth-flow.js +157 -0
- package/package.json +1 -1
- package/plugins/catalog.json +9 -0
- package/themes/dark-teal.css +63 -22
- package/web/dist/assets/index-DePnXpq-.js +374 -0
- package/web/dist/assets/index-F86qWlJy.css +41 -0
- package/web/dist/index.html +3 -2
- package/web/dist/assets/TerminalPanel-BytY8dx7.js +0 -6
- package/web/dist/assets/TerminalPanel-DOrYoP_4.css +0 -32
- package/web/dist/assets/index-CKoVyDkP.css +0 -10
- package/web/dist/assets/index-Dmwji4Cr.js +0 -361
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
function promptPayload(prompt) {
|
|
3
|
+
if (prompt.type === "select") {
|
|
4
|
+
return {
|
|
5
|
+
type: "select",
|
|
6
|
+
message: prompt.message,
|
|
7
|
+
options: prompt.options.map((option) => ({ ...option })),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
type: prompt.type,
|
|
12
|
+
message: prompt.message,
|
|
13
|
+
...(prompt.placeholder === undefined ? {} : { placeholder: prompt.placeholder }),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function eventPayload(event) {
|
|
17
|
+
if (event.type === "info") {
|
|
18
|
+
return {
|
|
19
|
+
type: "info",
|
|
20
|
+
message: event.message,
|
|
21
|
+
...(event.links === undefined ? {} : { links: event.links.map((link) => ({ ...link })) }),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return { ...event };
|
|
25
|
+
}
|
|
26
|
+
export class ProviderOAuthFlowManager {
|
|
27
|
+
host;
|
|
28
|
+
flows = new Map();
|
|
29
|
+
disposed = false;
|
|
30
|
+
constructor(host) {
|
|
31
|
+
this.host = host;
|
|
32
|
+
}
|
|
33
|
+
start(provider) {
|
|
34
|
+
if (this.disposed)
|
|
35
|
+
return null;
|
|
36
|
+
const providerId = provider.trim();
|
|
37
|
+
const runtimeProvider = this.host.modelRuntime().getProvider(providerId);
|
|
38
|
+
if (!providerId || !runtimeProvider?.auth.oauth) {
|
|
39
|
+
this.host.emit({
|
|
40
|
+
type: "notice",
|
|
41
|
+
level: "error",
|
|
42
|
+
text: "该服务商不支持 OAuth 登录",
|
|
43
|
+
textEn: "This provider does not support OAuth login",
|
|
44
|
+
});
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const existing = [...this.flows.values()].find((flow) => flow.provider === providerId);
|
|
48
|
+
if (existing)
|
|
49
|
+
return existing.id;
|
|
50
|
+
const flow = { id: randomUUID(), provider: providerId, abort: new AbortController() };
|
|
51
|
+
this.flows.set(flow.id, flow);
|
|
52
|
+
this.host.emit({ type: "provider_oauth_started", flowId: flow.id, provider: providerId });
|
|
53
|
+
void this.run(flow);
|
|
54
|
+
return flow.id;
|
|
55
|
+
}
|
|
56
|
+
reply(flowId, promptId, value) {
|
|
57
|
+
const flow = this.flows.get(flowId);
|
|
58
|
+
if (!flow?.prompt || flow.prompt.id !== promptId)
|
|
59
|
+
return;
|
|
60
|
+
const pending = flow.prompt;
|
|
61
|
+
flow.prompt = undefined;
|
|
62
|
+
pending.cleanup();
|
|
63
|
+
pending.resolve(value);
|
|
64
|
+
}
|
|
65
|
+
cancel(flowId) {
|
|
66
|
+
this.flows.get(flowId)?.abort.abort();
|
|
67
|
+
}
|
|
68
|
+
list() {
|
|
69
|
+
this.host.emit({
|
|
70
|
+
type: "provider_oauth_flows",
|
|
71
|
+
flows: [...this.flows.values()].map((flow) => ({
|
|
72
|
+
flowId: flow.id,
|
|
73
|
+
provider: flow.provider,
|
|
74
|
+
...(flow.prompt ? { promptId: flow.prompt.id, prompt: flow.prompt.payload } : {}),
|
|
75
|
+
...(flow.event ? { event: flow.event } : {}),
|
|
76
|
+
})),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
dispose() {
|
|
80
|
+
if (this.disposed)
|
|
81
|
+
return;
|
|
82
|
+
this.disposed = true;
|
|
83
|
+
for (const flow of this.flows.values())
|
|
84
|
+
flow.abort.abort();
|
|
85
|
+
this.flows.clear();
|
|
86
|
+
}
|
|
87
|
+
async run(flow) {
|
|
88
|
+
try {
|
|
89
|
+
await this.host.modelRuntime().login(flow.provider, "oauth", {
|
|
90
|
+
signal: flow.abort.signal,
|
|
91
|
+
prompt: (prompt) => this.prompt(flow, prompt),
|
|
92
|
+
notify: (event) => {
|
|
93
|
+
if (this.disposed || this.host.isDisposed())
|
|
94
|
+
return;
|
|
95
|
+
flow.event = eventPayload(event);
|
|
96
|
+
this.host.emit({
|
|
97
|
+
type: "provider_oauth_event",
|
|
98
|
+
flowId: flow.id,
|
|
99
|
+
provider: flow.provider,
|
|
100
|
+
event: flow.event,
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
if (this.disposed || this.host.isDisposed())
|
|
105
|
+
return;
|
|
106
|
+
await this.host.onLoginSuccess(flow.provider);
|
|
107
|
+
this.host.emit({ type: "provider_oauth_result", flowId: flow.id, provider: flow.provider, ok: true });
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (this.disposed || this.host.isDisposed())
|
|
111
|
+
return;
|
|
112
|
+
const cancelled = flow.abort.signal.aborted;
|
|
113
|
+
this.host.emit({
|
|
114
|
+
type: "provider_oauth_result",
|
|
115
|
+
flowId: flow.id,
|
|
116
|
+
provider: flow.provider,
|
|
117
|
+
ok: false,
|
|
118
|
+
...(cancelled ? { cancelled: true } : { error: error instanceof Error ? error.message : String(error) }),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
flow.prompt?.cleanup();
|
|
123
|
+
this.flows.delete(flow.id);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
prompt(flow, prompt) {
|
|
127
|
+
if (this.disposed || this.host.isDisposed() || flow.abort.signal.aborted || prompt.signal?.aborted) {
|
|
128
|
+
return Promise.reject(new Error("OAuth prompt cancelled"));
|
|
129
|
+
}
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const promptId = randomUUID();
|
|
132
|
+
const payload = promptPayload(prompt);
|
|
133
|
+
const abortPrompt = () => {
|
|
134
|
+
if (flow.prompt?.id !== promptId)
|
|
135
|
+
return;
|
|
136
|
+
const pending = flow.prompt;
|
|
137
|
+
flow.prompt = undefined;
|
|
138
|
+
pending?.cleanup();
|
|
139
|
+
reject(new Error("OAuth prompt cancelled"));
|
|
140
|
+
};
|
|
141
|
+
const cleanup = () => {
|
|
142
|
+
flow.abort.signal.removeEventListener("abort", abortPrompt);
|
|
143
|
+
prompt.signal?.removeEventListener("abort", abortPrompt);
|
|
144
|
+
};
|
|
145
|
+
flow.prompt = { id: promptId, payload, resolve, reject, cleanup };
|
|
146
|
+
flow.abort.signal.addEventListener("abort", abortPrompt, { once: true });
|
|
147
|
+
prompt.signal?.addEventListener("abort", abortPrompt, { once: true });
|
|
148
|
+
this.host.emit({
|
|
149
|
+
type: "provider_oauth_prompt",
|
|
150
|
+
flowId: flow.id,
|
|
151
|
+
provider: flow.provider,
|
|
152
|
+
promptId,
|
|
153
|
+
prompt: payload,
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.87.0",
|
|
4
4
|
"description": "Web chat interface for the pi coding agent, powered by the pi SDK (@earendil-works/pi-coding-agent) — one-command run, Docker/systemd/launchd deployable",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
package/plugins/catalog.json
CHANGED
|
@@ -62,6 +62,15 @@
|
|
|
62
62
|
"source": "xing-shuyin/pi-web-ui/plugins/wechat-ilink",
|
|
63
63
|
"homepage": "https://github.com/xing-shuyin/pi-web-ui/tree/main/plugins/wechat-ilink"
|
|
64
64
|
},
|
|
65
|
+
{
|
|
66
|
+
"id": "voice-input",
|
|
67
|
+
"name": "语音输入",
|
|
68
|
+
"icon": "🎤",
|
|
69
|
+
"description": "语音输入:输入框旁的麦克风按钮,浏览器语音识别直接听写进输入框;不支持/识别失败时自动降级为服务端转写(远端接口,或一键安装的本地 Whisper,免费不出网)。",
|
|
70
|
+
"descriptionEn": "Voice input: a mic button next to the composer. Browser speech recognition dictates into the input box, with server-side transcription as automatic fallback (remote API, or one-click local Whisper that runs fully offline).",
|
|
71
|
+
"source": "xing-shuyin/pi-web-ui/plugins/voice-input",
|
|
72
|
+
"homepage": "https://github.com/xing-shuyin/pi-web-ui/tree/main/plugins/voice-input"
|
|
73
|
+
},
|
|
65
74
|
{
|
|
66
75
|
"id": "image-toolkit",
|
|
67
76
|
"name": "image-toolkit",
|
package/themes/dark-teal.css
CHANGED
|
@@ -11,15 +11,15 @@
|
|
|
11
11
|
--text-dim: #a8a8b3;
|
|
12
12
|
--text-faint: #6e6e7a;
|
|
13
13
|
--accent: #31ac9d;
|
|
14
|
-
--accent-soft:
|
|
15
|
-
--green: #2dd4bf;
|
|
16
|
-
--green-soft:
|
|
14
|
+
--accent-soft: #3A8C8A26;
|
|
15
|
+
--green: #2dd4bf;
|
|
16
|
+
--green-soft: #4ADE801F;
|
|
17
17
|
--amber: #eab380;
|
|
18
18
|
--term-bg: #0b0b0e;
|
|
19
19
|
--term-fg: #ededf0;
|
|
20
20
|
--term-cursor: #2dd4bf;
|
|
21
21
|
--term-cursor-accent: #0b0b0e;
|
|
22
|
-
--term-selection:
|
|
22
|
+
--term-selection: #2DD4BF47;
|
|
23
23
|
--term-black: #1c1c22;
|
|
24
24
|
--term-green: #4ade80;
|
|
25
25
|
--term-cyan: #2dd4bf;
|
|
@@ -51,7 +51,6 @@
|
|
|
51
51
|
--notice-err-border: #f87171;
|
|
52
52
|
--notice-warn-border: #fbbf24;
|
|
53
53
|
--notice-info-border: #2dd4bf;
|
|
54
|
-
/* 发送/停止按钮内置写死 color:#fff,白字压在亮青上只有 1.9:1 → 改用深青做底 */
|
|
55
54
|
--send-blue: #0f766e;
|
|
56
55
|
--send-blue-hover: #0b5a54;
|
|
57
56
|
--brand-grad-a: #28a696;
|
|
@@ -59,7 +58,7 @@
|
|
|
59
58
|
--on-amber: #12121a;
|
|
60
59
|
--search-active-fg: #0f0f12;
|
|
61
60
|
--switch-knob: #f5f5f7;
|
|
62
|
-
--bg-elev3:
|
|
61
|
+
--bg-elev3: #FFFFFF0D;
|
|
63
62
|
--control-fg: #a8a8b3;
|
|
64
63
|
--control-bg: #1c1c22;
|
|
65
64
|
--control-border: #33333d;
|
|
@@ -137,11 +136,11 @@
|
|
|
137
136
|
}
|
|
138
137
|
.hljs-addition {
|
|
139
138
|
color: #86efac;
|
|
140
|
-
background:
|
|
139
|
+
background: #4ADE8024;
|
|
141
140
|
}
|
|
142
141
|
.hljs-deletion {
|
|
143
142
|
color: #fca5a5;
|
|
144
|
-
background:
|
|
143
|
+
background: #F8717124;
|
|
145
144
|
}
|
|
146
145
|
|
|
147
146
|
/* 亮底上的按钮/徽标:内置写死 color:#fff,压亮青只有 1.9:1 → 统一换近黑墨色(只改颜色) */
|
|
@@ -170,40 +169,82 @@
|
|
|
170
169
|
color: #0f0f12;
|
|
171
170
|
}
|
|
172
171
|
|
|
173
|
-
|
|
172
|
+
|
|
173
|
+
/* 模板卡悬停阴影 */
|
|
174
|
+
.empty-template:hover {
|
|
175
|
+
box-shadow: 0 8px 22px #5DC7B040;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* 内置几处写死的紫描边,换成符合主题的青 */
|
|
179
|
+
.empty-template:hover,
|
|
180
|
+
.empty-template.add:hover,
|
|
174
181
|
.msg-user .msg-body,
|
|
175
182
|
.session-item.active,
|
|
176
183
|
.project-item.active,
|
|
177
184
|
.term-tab.active,
|
|
178
|
-
.set-save-btn
|
|
179
|
-
|
|
185
|
+
.set-save-btn,
|
|
186
|
+
.dd-refresh.accent,
|
|
187
|
+
.attach-chip,
|
|
188
|
+
.attachcard-mode.bridged,
|
|
189
|
+
.browser-control-modal .bc-cite,
|
|
190
|
+
.notice-info {
|
|
191
|
+
border-color: #5DC7B080;
|
|
180
192
|
}
|
|
181
193
|
.chip.newchat {
|
|
182
194
|
border-color: var(--accent);
|
|
183
195
|
}
|
|
184
196
|
|
|
185
|
-
/*
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
197
|
+
/* 对话框内,附件引用卡虚线颜色 */
|
|
198
|
+
/* 网页引用 */
|
|
199
|
+
.attachcard.page {
|
|
200
|
+
border-color: #38bdf866;
|
|
189
201
|
}
|
|
190
|
-
|
|
191
|
-
|
|
202
|
+
/* 其他附件引用 */
|
|
203
|
+
.attachcard {
|
|
204
|
+
border-color: #ff9c6266;
|
|
192
205
|
}
|
|
193
206
|
|
|
194
|
-
/*
|
|
207
|
+
/* 背景色 */
|
|
208
|
+
/* 统一对话、终端及 Git 侧栏背景色*/
|
|
195
209
|
.term-side,
|
|
196
210
|
.scm-view,
|
|
197
211
|
.scm-diff-header {
|
|
198
212
|
background: #121215;
|
|
199
213
|
}
|
|
214
|
+
/* html 预览顶栏 */
|
|
215
|
+
.fp-html-bar {
|
|
216
|
+
background: var(--accent-soft);
|
|
217
|
+
}
|
|
200
218
|
|
|
201
|
-
/*
|
|
219
|
+
/* 输入框发送\停止按钮阴影 */
|
|
220
|
+
.inputbox .btn.send,
|
|
221
|
+
.inputbox .btn.stop {
|
|
222
|
+
box-shadow: 0px 2px 14px #5dc7b040;
|
|
223
|
+
}
|
|
224
|
+
.inputbox .btn.send:hover:not(:disabled),
|
|
225
|
+
.inputbox .btn.stop:hover:not(:disabled) {
|
|
226
|
+
box-shadow: 0 8px 22px #5dc7b040;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* 使正文行内代码更显眼 */
|
|
202
230
|
.msg-text code {
|
|
203
231
|
background: #303038;
|
|
204
232
|
}
|
|
205
233
|
|
|
206
|
-
/*
|
|
207
|
-
.
|
|
208
|
-
|
|
234
|
+
/* 调整 MD 文件预览样式 */
|
|
235
|
+
.fp-markdown :not(pre)>code,
|
|
236
|
+
.dialog-inline .md :not(pre)>code {
|
|
237
|
+
color: var(--text);
|
|
238
|
+
background: #303038;
|
|
239
|
+
}
|
|
240
|
+
.fp-markdown blockquote {
|
|
241
|
+
border: 1px solid #5CF6D55E;
|
|
242
|
+
border-left-width: 4px;
|
|
243
|
+
background: #5cd6f612;
|
|
244
|
+
}
|
|
245
|
+
.fp-markdown thead {
|
|
246
|
+
background: #5cd6f612;
|
|
247
|
+
}
|
|
248
|
+
.fp-markdown {
|
|
249
|
+
background: radial-gradient(circle at 10% 0%, #5CCBF60D, transparent 34%), #00000038;
|
|
209
250
|
}
|