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.
Files changed (36) hide show
  1. package/CHANGELOG.md +59 -21
  2. package/README.md +1 -1
  3. package/bin/pi-web-ui.mjs +312 -40
  4. package/dist/server/agent-service.js +297 -6
  5. package/dist/server/client-state.js +8 -0
  6. package/dist/server/composer-drafts.js +138 -0
  7. package/dist/server/dsh/dsh-agent-service.js +531 -54
  8. package/dist/server/dsh/dsh-client.js +28 -0
  9. package/dist/server/dsh/dsh-sessions.js +28 -0
  10. package/dist/server/dsh/dsh-usage.js +82 -0
  11. package/dist/server/dsh/preset-clones.js +260 -0
  12. package/dist/server/dsh/runtime/custom-prompt.mjs +33 -0
  13. package/dist/server/dsh/runtime/goal-rpc.mjs +406 -22
  14. package/dist/server/dsh/runtime/launcher.mjs +17 -0
  15. package/dist/server/dsh/runtime/override.patch.yml +19 -1
  16. package/dist/server/files-service.js +259 -1
  17. package/dist/server/index.js +219 -3
  18. package/dist/server/mcp-bridge.js +126 -22
  19. package/dist/server/mcp-hot-reload.js +117 -0
  20. package/dist/server/model-admin.js +93 -3
  21. package/dist/server/plugin-dom.js +83 -0
  22. package/dist/server/plugin-facilities.js +15 -1
  23. package/dist/server/plugin-installer.js +6 -0
  24. package/dist/server/plugins.js +781 -74
  25. package/dist/server/protocol-version.js +1 -1
  26. package/dist/server/provider-oauth-flow.js +157 -0
  27. package/package.json +1 -1
  28. package/plugins/catalog.json +9 -0
  29. package/themes/dark-teal.css +63 -22
  30. package/web/dist/assets/index-DePnXpq-.js +374 -0
  31. package/web/dist/assets/index-F86qWlJy.css +41 -0
  32. package/web/dist/index.html +3 -2
  33. package/web/dist/assets/TerminalPanel-BytY8dx7.js +0 -6
  34. package/web/dist/assets/TerminalPanel-DOrYoP_4.css +0 -32
  35. package/web/dist/assets/index-CKoVyDkP.css +0 -10
  36. package/web/dist/assets/index-Dmwji4Cr.js +0 -361
@@ -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 = 15;
11
+ export const PROTOCOL_VERSION = 17;
@@ -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.86.2",
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": {
@@ -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",
@@ -11,15 +11,15 @@
11
11
  --text-dim: #a8a8b3;
12
12
  --text-faint: #6e6e7a;
13
13
  --accent: #31ac9d;
14
- --accent-soft: rgb(58 140 138 / 15%);
15
- --green: #2dd4bf; /* 成功色:刻意比 accent 的青更亮一档,非笔误 */
16
- --green-soft: rgba(74, 222, 128, 0.12);
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: rgba(45, 212, 191, 0.28);
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: rgba(255, 255, 255, 0.05);
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: rgba(74, 222, 128, 0.14);
139
+ background: #4ADE8024;
141
140
  }
142
141
  .hljs-deletion {
143
142
  color: #fca5a5;
144
- background: rgba(248, 113, 113, 0.14);
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
- /* 内置这几处写死紫描边 rgba(139,92,246,.4) → 换成符合主题的青(只改 border-color) */
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
- border-color: rgb(93 199 176 / 50%);
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
- .empty-template:hover {
187
- border-color: #5dc7b030;
188
- box-shadow: 0 8px 22px #5dc7b040;
197
+ /* 对话框内,附件引用卡虚线颜色 */
198
+ /* 网页引用 */
199
+ .attachcard.page {
200
+ border-color: #38bdf866;
189
201
  }
190
- .empty-template.add:hover {
191
- border-color: #5dc7b099;
202
+ /* 其他附件引用 */
203
+ .attachcard {
204
+ border-color: #ff9c6266;
192
205
  }
193
206
 
194
- /* 工具视图(终端侧栏 / SCM)内置写死 var(--bg-elev) → 并入面板层 #121215(= 62% #17171c 叠 #0a0a0a)*/
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
- /* 让消息正文行内代码更显眼:内置 --bg-elev2 与面板只差 4 级 → 单独抬到 #303038 */
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
- /* 附件卡虚线框内置是紫 与待发图片 chip 同一支梅红(只改 border-color) */
207
- .attachcard {
208
- border-color: #ec489973;
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
  }