pi-web-ui 0.26.0 → 0.26.2

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.
@@ -1,78 +1,88 @@
1
- # pi-web-ui behind nginx at a sub-path: http://<host>:83/pi/
2
- # Backend app: http://127.0.0.1:8787 (default PORT env)
3
- #
4
- # Topology:
5
- # LAN users → http://192.168.1.101:83/pi/ (plain HTTP listener)
6
- # frp (public) → 39.99.235.208:60018 127.0.0.1:83 (PROXY protocol v2)
7
- #
8
- # frpc sends PROXY v2 to nginx (transport.proxyProtocolVersion = "v2"), so
9
- # direct LAN connections MUST NOT require it — two listeners, one port:
10
- # * 127.0.0.1:83 proxy_protocol only the local frp client connects
11
- # here (it speaks PROXY v2; nginx then sees the real visitor IP).
12
- # * 192.168.1.101:83 plain HTTP — LAN browsers, no PROXY header needed.
13
- #
14
- # The frontend uses absolute paths (/ws WebSocket, /assets/*, /favicon.svg,
15
- # /api/file…) so those get their own proxied locations next to /pi/.
16
-
17
- # Reuse for Upgrade/Connection headers (WebSocket).
18
- map $http_upgrade $connection_upgrade {
19
- default upgrade;
20
- '' close;
21
- }
22
-
23
- server {
24
- listen 127.0.0.1:83 proxy_protocol;
25
- listen 192.168.1.101:83;
26
-
27
- server_name _;
28
-
29
- # Trust PROXY-protocol headers only from the local frp client; plain
30
- # (LAN) connections keep their real $remote_addr untouched.
31
- set_real_ip_from 127.0.0.1;
32
- real_ip_header proxy_protocol;
33
-
34
- # ---- main entry: strip /pi/ and forward to the app root ----
35
- location /pi/ {
36
- proxy_pass http://127.0.0.1:8787/;
37
- proxy_http_version 1.1;
38
- proxy_set_header Host $host;
39
- proxy_set_header X-Real-IP $remote_addr;
40
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
41
- proxy_set_header X-Forwarded-Proto $scheme;
42
- proxy_set_header Upgrade $http_upgrade;
43
- proxy_set_header Connection $connection_upgrade;
44
- }
45
-
46
- # ---- WebSocket (the frontend connects to ws://<host>/ws) ----
47
- location /ws {
48
- proxy_pass http://127.0.0.1:8787;
49
- proxy_http_version 1.1;
50
- proxy_set_header Upgrade $http_upgrade;
51
- proxy_set_header Connection $connection_upgrade;
52
- proxy_read_timeout 3600s;
53
- proxy_send_timeout 3600s;
54
- proxy_set_header X-Real-IP $remote_addr;
55
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
56
- }
57
-
58
- # ---- absolute asset paths baked into index.html ----
59
- location /assets/ {
60
- proxy_pass http://127.0.0.1:8787;
61
- }
62
- location = /favicon.svg {
63
- proxy_pass http://127.0.0.1:8787;
64
- }
65
- location = /favicon-streaming.svg {
66
- proxy_pass http://127.0.0.1:8787;
67
- }
68
-
69
- # ---- media preview / download / health API ----
70
- location /api/ {
71
- proxy_pass http://127.0.0.1:8787;
72
- }
73
-
74
- # bare root → app entry
75
- location = / {
76
- return 302 /pi/;
77
- }
78
- }
1
+ # pi-web-ui behind nginx at a sub-path: http://<host>:83/pi/
2
+ # Backend app: http://127.0.0.1:8787 (default PORT env)
3
+ #
4
+ # IMPORTANT: pi-web-ui 0.23+ checks the WebSocket Origin against the request
5
+ # Host (hostname AND port). Every proxied location MUST forward the original
6
+ # Host with $http_host (keeps the port). Using $host (drops the port) or
7
+ # leaving Host unset (defaults to 127.0.0.1:8787) makes the upgrade fail with
8
+ # 403 the page loads but chat/terminal keep reconnecting.
9
+ #
10
+ # Topology (two listeners on one port: frp + LAN coexist):
11
+ # frp (public) -> <PUBLIC_IP>:<PUBLIC_PORT> -> 127.0.0.1:83 (PROXY protocol v2)
12
+ # LAN users -> http://<LAN_IP>:83/pi/ (plain HTTP listener)
13
+ #
14
+ # frpc sends PROXY v2 to nginx (transport.proxyProtocolVersion = "v2"):
15
+ # * 127.0.0.1:83 proxy_protocol only the local frp client connects
16
+ # here (it speaks PROXY v2; nginx then sees the real visitor IP).
17
+ # * <LAN_IP>:83 plain HTTP LAN browsers, no PROXY header needed.
18
+ #
19
+ # Simpler alternative (no real client IPs): drop proxy_protocol entirely and
20
+ # use a single `listen 83;` — then frpc must NOT set proxyProtocolVersion.
21
+ #
22
+ # The frontend uses absolute paths (/ws WebSocket, /assets/*, /favicon.svg,
23
+ # /api/file…) so those get their own proxied locations next to /pi/.
24
+
25
+ # Reuse for Upgrade/Connection headers (WebSocket).
26
+ map $http_upgrade $connection_upgrade {
27
+ default upgrade;
28
+ '' close;
29
+ }
30
+
31
+ server {
32
+ listen 127.0.0.1:83 proxy_protocol;
33
+ listen <LAN_IP>:83;
34
+
35
+ server_name _;
36
+
37
+ # Trust PROXY-protocol headers only from the local frp client; plain
38
+ # (LAN) connections keep their real $remote_addr untouched.
39
+ set_real_ip_from 127.0.0.1;
40
+ real_ip_header proxy_protocol;
41
+
42
+ # ---- main entry: strip /pi/ and forward to the app root ----
43
+ location /pi/ {
44
+ proxy_pass http://127.0.0.1:8787/;
45
+ proxy_http_version 1.1;
46
+ # $http_host keeps the port origin check compares hostname AND port.
47
+ proxy_set_header Host $http_host;
48
+ proxy_set_header X-Real-IP $remote_addr;
49
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
50
+ proxy_set_header X-Forwarded-Proto $scheme;
51
+ proxy_set_header Upgrade $http_upgrade;
52
+ proxy_set_header Connection $connection_upgrade;
53
+ }
54
+
55
+ # ---- WebSocket (the frontend connects to ws://<host>/ws) ----
56
+ location /ws {
57
+ proxy_pass http://127.0.0.1:8787;
58
+ proxy_http_version 1.1;
59
+ proxy_set_header Host $http_host;
60
+ proxy_set_header Upgrade $http_upgrade;
61
+ proxy_set_header Connection $connection_upgrade;
62
+ proxy_read_timeout 3600s;
63
+ proxy_send_timeout 3600s;
64
+ proxy_set_header X-Real-IP $remote_addr;
65
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
66
+ }
67
+
68
+ # ---- absolute asset paths baked into index.html ----
69
+ location /assets/ {
70
+ proxy_pass http://127.0.0.1:8787;
71
+ }
72
+ location = /favicon.svg {
73
+ proxy_pass http://127.0.0.1:8787;
74
+ }
75
+ location = /favicon-streaming.svg {
76
+ proxy_pass http://127.0.0.1:8787;
77
+ }
78
+
79
+ # ---- media preview / download / health API ----
80
+ location /api/ {
81
+ proxy_pass http://127.0.0.1:8787;
82
+ }
83
+
84
+ # bare root → app entry
85
+ location = / {
86
+ return 302 /pi/;
87
+ }
88
+ }
@@ -1,71 +1,71 @@
1
- <?xml version="1.0" encoding="UTF-16"?>
2
- <!--
3
- pi-web-ui Task Scheduler task — Windows auto-start at logon.
4
-
5
- The easy way (no admin needed, generates everything for you):
6
- npm i -g pi-web-ui
7
- pi-web-ui server install --port 8787 --cwd C:\path\to\project
8
- pi-web-ui server status | restart | stop | uninstall
9
-
10
- Manual install with this template (edit the paths below first):
11
- schtasks /Create /TN "pi-web-ui" /XML pi-web-ui-task.xml /F
12
- schtasks /Run /TN "pi-web-ui"
13
-
14
- Notes:
15
- - The task runs the PowerShell launcher the CLI generates at
16
- %APPDATA%\pi-web-ui\pi-web-ui.ps1 with -WindowStyle Hidden, so the
17
- server runs with no black console window (nothing to accidentally
18
- close/kill). The ps1 sets PORT/PI_WEB_CWD, cd's to the workspace,
19
- launches node, and appends output to %USERPROFILE%\pi-web-ui.log.
20
- Preview both generated files with: pi-web-ui server install --print
21
- - Save this file as UTF-16 LE (schtasks requires it; the CLI does this
22
- automatically when it writes the task XML).
23
- - LogonTrigger = starts when you log in, same as a launchd user agent.
24
- For boot-start without login, use Docker instead (see README).
25
- -->
26
- <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
27
- <RegistrationInfo>
28
- <Description>pi-web-ui — web chat for the pi coding agent (auto-start at logon)</Description>
29
- </RegistrationInfo>
30
- <Triggers>
31
- <LogonTrigger>
32
- <Enabled>true</Enabled>
33
- </LogonTrigger>
34
- </Triggers>
35
- <Principals>
36
- <Principal id="Author">
37
- <LogonType>InteractiveToken</LogonType>
38
- <RunLevel>LeastPrivilege</RunLevel>
39
- </Principal>
40
- </Principals>
41
- <Settings>
42
- <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
43
- <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
44
- <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
45
- <AllowHardTerminate>true</AllowHardTerminate>
46
- <StartWhenAvailable>false</StartWhenAvailable>
47
- <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
48
- <IdleSettings>
49
- <StopOnIdleEnd>false</StopOnIdleEnd>
50
- <RestartOnIdle>false</RestartOnIdle>
51
- </IdleSettings>
52
- <AllowStartOnDemand>true</AllowStartOnDemand>
53
- <Enabled>true</Enabled>
54
- <Hidden>false</Hidden>
55
- <RunOnlyIfIdle>false</RunOnlyIfIdle>
56
- <WakeToRun>false</WakeToRun>
57
- <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
58
- <Priority>7</Priority>
59
- <RestartOnFailure>
60
- <Interval>PT1M</Interval>
61
- <Count>3</Count>
62
- </RestartOnFailure>
63
- </Settings>
64
- <Actions Context="Author">
65
- <Exec>
66
- <Command>C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe</Command>
67
- <Arguments>-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "%APPDATA%\pi-web-ui\pi-web-ui.ps1"</Arguments>
68
- <WorkingDirectory>%USERPROFILE%</WorkingDirectory>
69
- </Exec>
70
- </Actions>
71
- </Task>
1
+ <?xml version="1.0" encoding="UTF-16"?>
2
+ <!--
3
+ pi-web-ui Task Scheduler task — Windows auto-start at logon.
4
+
5
+ The easy way (no admin needed, generates everything for you):
6
+ npm i -g pi-web-ui
7
+ pi-web-ui server install --port 8787 --cwd C:\path\to\project
8
+ pi-web-ui server status | restart | stop | uninstall
9
+
10
+ Manual install with this template (edit the paths below first):
11
+ schtasks /Create /TN "pi-web-ui" /XML pi-web-ui-task.xml /F
12
+ schtasks /Run /TN "pi-web-ui"
13
+
14
+ Notes:
15
+ - The task runs the PowerShell launcher the CLI generates at
16
+ %APPDATA%\pi-web-ui\pi-web-ui.ps1 with -WindowStyle Hidden, so the
17
+ server runs with no black console window (nothing to accidentally
18
+ close/kill). The ps1 sets PORT/PI_WEB_CWD, cd's to the workspace,
19
+ launches node, and appends output to %USERPROFILE%\pi-web-ui.log.
20
+ Preview both generated files with: pi-web-ui server install --print
21
+ - Save this file as UTF-16 LE (schtasks requires it; the CLI does this
22
+ automatically when it writes the task XML).
23
+ - LogonTrigger = starts when you log in, same as a launchd user agent.
24
+ For boot-start without login, use Docker instead (see README).
25
+ -->
26
+ <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
27
+ <RegistrationInfo>
28
+ <Description>pi-web-ui — web chat for the pi coding agent (auto-start at logon)</Description>
29
+ </RegistrationInfo>
30
+ <Triggers>
31
+ <LogonTrigger>
32
+ <Enabled>true</Enabled>
33
+ </LogonTrigger>
34
+ </Triggers>
35
+ <Principals>
36
+ <Principal id="Author">
37
+ <LogonType>InteractiveToken</LogonType>
38
+ <RunLevel>LeastPrivilege</RunLevel>
39
+ </Principal>
40
+ </Principals>
41
+ <Settings>
42
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
43
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
44
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
45
+ <AllowHardTerminate>true</AllowHardTerminate>
46
+ <StartWhenAvailable>false</StartWhenAvailable>
47
+ <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
48
+ <IdleSettings>
49
+ <StopOnIdleEnd>false</StopOnIdleEnd>
50
+ <RestartOnIdle>false</RestartOnIdle>
51
+ </IdleSettings>
52
+ <AllowStartOnDemand>true</AllowStartOnDemand>
53
+ <Enabled>true</Enabled>
54
+ <Hidden>false</Hidden>
55
+ <RunOnlyIfIdle>false</RunOnlyIfIdle>
56
+ <WakeToRun>false</WakeToRun>
57
+ <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
58
+ <Priority>7</Priority>
59
+ <RestartOnFailure>
60
+ <Interval>PT1M</Interval>
61
+ <Count>3</Count>
62
+ </RestartOnFailure>
63
+ </Settings>
64
+ <Actions Context="Author">
65
+ <Exec>
66
+ <Command>C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe</Command>
67
+ <Arguments>-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "%APPDATA%\pi-web-ui\pi-web-ui.ps1"</Arguments>
68
+ <WorkingDirectory>%USERPROFILE%</WorkingDirectory>
69
+ </Exec>
70
+ </Actions>
71
+ </Task>
@@ -1,31 +1,31 @@
1
- # pi-web-ui systemd unit — Linux auto-start on boot.
2
- #
3
- # Install (adjust User/WorkingDirectory/Environment to taste):
4
- # sudo npm i -g pi-web-ui
5
- # sudo cp deploy/pi-web-ui.service /etc/systemd/system/
6
- # sudo systemctl daemon-reload
7
- # sudo systemctl enable --now pi-web-ui
8
- # sudo systemctl status pi-web-ui
9
- #
10
- # Logs: journalctl -u pi-web-ui -f
11
-
12
- [Unit]
13
- Description=pi-web-ui — web chat for the pi coding agent
14
- After=network.target
15
-
16
- [Service]
17
- Type=simple
18
- # Run as an unprivileged user (change to your user, e.g. yourname)
19
- User=YOUR_USER
20
- # The workspace the agent operates in (read/edit/bash/write)
21
- WorkingDirectory=/home/YOUR_USER
22
- Environment=PORT=8787
23
- # Point at your pi config dir if it's not the default ~/.pi/agent
24
- #Environment=PI_CODING_AGENT_DIR=/home/YOUR_USER/.pi/agent
25
- ExecStart=/usr/bin/pi-web-ui
26
- Restart=on-failure
27
- RestartSec=5
28
- # npm global bin may live elsewhere (nvm, etc.) — find with: which pi-web-ui
29
-
30
- [Install]
31
- WantedBy=multi-user.target
1
+ # pi-web-ui systemd unit — Linux auto-start on boot.
2
+ #
3
+ # Install (adjust User/WorkingDirectory/Environment to taste):
4
+ # sudo npm i -g pi-web-ui
5
+ # sudo cp deploy/pi-web-ui.service /etc/systemd/system/
6
+ # sudo systemctl daemon-reload
7
+ # sudo systemctl enable --now pi-web-ui
8
+ # sudo systemctl status pi-web-ui
9
+ #
10
+ # Logs: journalctl -u pi-web-ui -f
11
+
12
+ [Unit]
13
+ Description=pi-web-ui — web chat for the pi coding agent
14
+ After=network.target
15
+
16
+ [Service]
17
+ Type=simple
18
+ # Run as an unprivileged user (change to your user, e.g. yourname)
19
+ User=YOUR_USER
20
+ # The workspace the agent operates in (read/edit/bash/write)
21
+ WorkingDirectory=/home/YOUR_USER
22
+ Environment=PORT=8787
23
+ # Point at your pi config dir if it's not the default ~/.pi/agent
24
+ #Environment=PI_CODING_AGENT_DIR=/home/YOUR_USER/.pi/agent
25
+ ExecStart=/usr/bin/pi-web-ui
26
+ Restart=on-failure
27
+ RestartSec=5
28
+ # npm global bin may live elsewhere (nvm, etc.) — find with: which pi-web-ui
29
+
30
+ [Install]
31
+ WantedBy=multi-user.target
@@ -274,14 +274,14 @@ function sniffImageMime(buf, ext) {
274
274
  * programs wait for input that never comes. Legacy Chinese files are often
275
275
  * GBK/GB2312 — read them with the right encoding, never paste mojibake into
276
276
  * reasoning/answers. */
277
- 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:
278
-
279
-
280
-
281
- - 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.
282
- - 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.
283
- - 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\`).
284
-
277
+ 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:
278
+
279
+
280
+
281
+ - 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.
282
+ - 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.
283
+ - 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\`).
284
+
285
285
  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.`;
286
286
  /**
287
287
  * Killable bash tool: wraps the SDK bash tool with operations that register
@@ -2191,6 +2191,25 @@ export class ClientSession {
2191
2191
  };
2192
2192
  mkdirSync(this.agentDir, { recursive: true });
2193
2193
  writeFileSync(this.modelsConfigPath(), JSON.stringify({ providers }, null, 2) + "\n");
2194
+ // Allow a custom models.json entry to reuse the provider credential
2195
+ // already stored in auth.json. Seed the shared runtime too, because
2196
+ // older pi-ai versions did not always fall back to stored credentials
2197
+ // for a newly-created custom provider. Never copy the secret into
2198
+ // models.json.
2199
+ try {
2200
+ const auth = JSON.parse(readFileSync(join(this.agentDir, "auth.json"), "utf8"));
2201
+ const credential = auth[pid];
2202
+ if (credential &&
2203
+ typeof credential === "object" &&
2204
+ "key" in credential &&
2205
+ typeof credential.key === "string" &&
2206
+ credential.key.trim()) {
2207
+ await this.runtime.services.modelRuntime.setRuntimeApiKey(pid, credential.key);
2208
+ }
2209
+ }
2210
+ catch {
2211
+ // auth.json is optional; models.json can still use its own apiKey.
2212
+ }
2194
2213
  await this.runtime.services.modelRuntime.refresh();
2195
2214
  await this.listModelsConfig();
2196
2215
  await this.listModels();
@@ -3232,9 +3251,9 @@ export class ClientSession {
3232
3251
  content: [
3233
3252
  {
3234
3253
  type: "text",
3235
- text: `
3236
- <vision-bridge>
3237
- ${transcript}
3254
+ text: `
3255
+ <vision-bridge>
3256
+ ${transcript}
3238
3257
  </vision-bridge>`,
3239
3258
  },
3240
3259
  ...(pathImg
@@ -4206,6 +4225,54 @@ ${transcript}
4206
4225
  });
4207
4226
  }
4208
4227
  }
4228
+ /** Save text from the file preview panel within the active workspace. */
4229
+ async writeFile(relPath, text) {
4230
+ try {
4231
+ const root = resolve(this.cwd);
4232
+ const wp = workspacePath(root, relPath);
4233
+ if (!wp) {
4234
+ this.emit({
4235
+ type: "notice",
4236
+ level: "warning",
4237
+ text: `路径超出工作区:${relPath}`,
4238
+ });
4239
+ return;
4240
+ }
4241
+ if (Buffer.byteLength(text, "utf8") > 2 * 1024 * 1024) {
4242
+ this.emit({
4243
+ type: "notice",
4244
+ level: "warning",
4245
+ text: "文件内容过大,无法保存(上限 2MB)",
4246
+ });
4247
+ return;
4248
+ }
4249
+ const stat = statSync(wp.abs);
4250
+ if (!stat.isFile()) {
4251
+ this.emit({
4252
+ type: "notice",
4253
+ level: "warning",
4254
+ text: `不是文件:${relPath}`,
4255
+ });
4256
+ return;
4257
+ }
4258
+ writeFileSync(wp.abs, text, "utf8");
4259
+ this.emit({
4260
+ type: "notice",
4261
+ level: "info",
4262
+ text: `已保存:${wp.rel}`,
4263
+ });
4264
+ // Re-read through the same path as the preview request so the client
4265
+ // gets the canonical content, line count and file size after saving.
4266
+ await this.readFile(wp.rel);
4267
+ }
4268
+ catch (err) {
4269
+ this.emit({
4270
+ type: "notice",
4271
+ level: "error",
4272
+ text: `保存文件失败:${err.message}`,
4273
+ });
4274
+ }
4275
+ }
4209
4276
  async cycleModel() {
4210
4277
  try {
4211
4278
  await this.session.cycleModel();
@@ -376,6 +376,9 @@ wss.on("connection", (ws) => {
376
376
  case "read_file":
377
377
  void cs.readFile(msg.path);
378
378
  break;
379
+ case "write_file":
380
+ void cs.writeFile(msg.path, msg.text);
381
+ break;
379
382
  case "list_models":
380
383
  void cs.listModels();
381
384
  break;