pi-web-ui 0.44.2 → 0.45.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.
@@ -1,48 +1,48 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
- <!--
4
- pi-web-ui launchd agent — macOS auto-start at login.
5
-
6
- Install:
7
- npm i -g pi-web-ui
8
- cp deploy/com.xingshuyin.pi-web-ui.plist ~/Library/LaunchAgents/
9
- # edit ProgramArguments / WorkingDirectory / PI_WEB_CWD for your setup
10
- launchctl load ~/Library/LaunchAgents/com.xingshuyin.pi-web-ui.plist
11
- # (or: launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.xingshuyin.pi-web-ui.plist)
12
-
13
- Find the pi-web-ui binary path with: which pi-web-ui
14
- -->
15
- <plist version="1.0">
16
- <dict>
17
- <key>Label</key>
18
- <string>com.xingshuyin.pi-web-ui</string>
19
-
20
- <key>ProgramArguments</key>
21
- <array>
22
- <string>/usr/local/bin/pi-web-ui</string>
23
- </array>
24
-
25
- <key>RunAtLoad</key>
26
- <true/>
27
-
28
- <!-- Restart if it crashes -->
29
- <key>KeepAlive</key>
30
- <true/>
31
-
32
- <key>WorkingDirectory</key>
33
- <string>/Users/YOUR_USER</string>
34
-
35
- <key>EnvironmentVariables</key>
36
- <dict>
37
- <key>PORT</key>
38
- <string>8787</string>
39
- <key>PI_WEB_CWD</key>
40
- <string>/Users/YOUR_USER</string>
41
- </dict>
42
-
43
- <key>StandardOutPath</key>
44
- <string>/tmp/pi-web-ui.log</string>
45
- <key>StandardErrorPath</key>
46
- <string>/tmp/pi-web-ui.err</string>
47
- </dict>
48
- </plist>
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <!--
4
+ pi-web-ui launchd agent — macOS auto-start at login.
5
+
6
+ Install:
7
+ npm i -g pi-web-ui
8
+ cp deploy/com.xingshuyin.pi-web-ui.plist ~/Library/LaunchAgents/
9
+ # edit ProgramArguments / WorkingDirectory / PI_WEB_CWD for your setup
10
+ launchctl load ~/Library/LaunchAgents/com.xingshuyin.pi-web-ui.plist
11
+ # (or: launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.xingshuyin.pi-web-ui.plist)
12
+
13
+ Find the pi-web-ui binary path with: which pi-web-ui
14
+ -->
15
+ <plist version="1.0">
16
+ <dict>
17
+ <key>Label</key>
18
+ <string>com.xingshuyin.pi-web-ui</string>
19
+
20
+ <key>ProgramArguments</key>
21
+ <array>
22
+ <string>/usr/local/bin/pi-web-ui</string>
23
+ </array>
24
+
25
+ <key>RunAtLoad</key>
26
+ <true/>
27
+
28
+ <!-- Restart if it crashes -->
29
+ <key>KeepAlive</key>
30
+ <true/>
31
+
32
+ <key>WorkingDirectory</key>
33
+ <string>/Users/YOUR_USER</string>
34
+
35
+ <key>EnvironmentVariables</key>
36
+ <dict>
37
+ <key>PORT</key>
38
+ <string>8787</string>
39
+ <key>PI_WEB_CWD</key>
40
+ <string>/Users/YOUR_USER</string>
41
+ </dict>
42
+
43
+ <key>StandardOutPath</key>
44
+ <string>/tmp/pi-web-ui.log</string>
45
+ <key>StandardErrorPath</key>
46
+ <string>/tmp/pi-web-ui.err</string>
47
+ </dict>
48
+ </plist>
@@ -1,88 +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
- # 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
+ # 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
@@ -10,7 +10,7 @@
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
- import { spawn } from "node:child_process";
13
+ import { spawn, spawnSync } from "node:child_process";
14
14
  import { existsSync, readFileSync, rmSync, statSync, writeFileSync, mkdirSync, watch, } from "node:fs";
15
15
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
16
16
  import { fileURLToPath } from "node:url";
@@ -80,14 +80,14 @@ export class QuiesceRejectedError extends Error {
80
80
  * programs wait for input that never comes. Legacy Chinese files are often
81
81
  * GBK/GB2312 — read them with the right encoding, never paste mojibake into
82
82
  * reasoning/answers. */
83
- 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:
84
-
85
-
86
-
87
- - 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.
88
- - 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.
89
- - 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\`).
90
-
83
+ 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:
84
+
85
+
86
+
87
+ - 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.
88
+ - 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.
89
+ - 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\`).
90
+
91
91
  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.`;
92
92
  /**
93
93
  * Killable bash tool: wraps the SDK bash tool with operations that register
@@ -550,6 +550,8 @@ export class ClientSession {
550
550
  flushSnapshot: () => this.flushSnapshot(),
551
551
  isDisposed: () => this.disposed,
552
552
  getSession: () => this.session,
553
+ cwd: () => this.cwd,
554
+ agentDir: () => this.agentDir,
553
555
  isStreaming: () => this.session.isStreaming,
554
556
  reloadSession: async () => {
555
557
  await this.session.reload();
@@ -1251,6 +1253,7 @@ export class ClientSession {
1251
1253
  tools: state.tools.map((t) => t.name),
1252
1254
  version: ++this.version,
1253
1255
  piConfigured: this.isPiConfigured(),
1256
+ piAgentInstalled: this.isPiCliInstalled(),
1254
1257
  stats,
1255
1258
  };
1256
1259
  }
@@ -1336,6 +1339,38 @@ export class ClientSession {
1336
1339
  this.piCheckCache = { at: now, configured };
1337
1340
  return configured;
1338
1341
  }
1342
+ /**
1343
+ * Whether the pi CLI binary is installed and runnable (`pi --version`
1344
+ * probe). Cached machine-wide (same binary for every client) for 10s —
1345
+ * the check is only rerun after install or when the cache expires.
1346
+ */
1347
+ static piCliProbe = null;
1348
+ static PI_CLI_PROBE_TTL_MS = 10_000;
1349
+ isPiCliInstalled() {
1350
+ const now = Date.now();
1351
+ const cached = ClientSession.piCliProbe;
1352
+ if (cached && now - cached.at < ClientSession.PI_CLI_PROBE_TTL_MS)
1353
+ return cached.installed;
1354
+ let installed = false;
1355
+ try {
1356
+ const res = spawnSync("pi", ["--version"], {
1357
+ timeout: 5000,
1358
+ stdio: "ignore",
1359
+ // Windows: `pi` resolves to a pi.cmd shim — spawnSync can only
1360
+ // exec those through a shell (else ENOENT).
1361
+ shell: process.platform === "win32",
1362
+ });
1363
+ installed = !res.error && res.status === 0;
1364
+ }
1365
+ catch {
1366
+ installed = false;
1367
+ }
1368
+ ClientSession.piCliProbe = { at: now, installed };
1369
+ return installed;
1370
+ }
1371
+ static invalidatePiCliProbe() {
1372
+ ClientSession.piCliProbe = null;
1373
+ }
1339
1374
  /**
1340
1375
  * Run a command async, collecting stdout+stderr; kills on timeout.
1341
1376
  * Never throws / never crashes the server: spawn errors (ENOENT etc.)
@@ -1483,6 +1518,9 @@ export class ClientSession {
1483
1518
  text: `pi agent 安装失败:${err.message}`,
1484
1519
  });
1485
1520
  }
1521
+ // The CLI may just have landed on PATH (or the install may have failed) —
1522
+ // drop the probe cache so the next snapshot re-checks.
1523
+ ClientSession.invalidatePiCliProbe();
1486
1524
  this.flushSnapshot();
1487
1525
  }
1488
1526
  /** Send a snapshot immediately (cancels any pending throttled one).
@@ -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
@@ -178,6 +178,7 @@ export class ClientStateStore {
178
178
  terminalBash: s?.settings?.terminalBash ?? false,
179
179
  terminalBashIdleMs: s?.settings?.terminalBashIdleMs ?? 15_000,
180
180
  thinkingWrap: s?.settings?.thinkingWrap ?? false,
181
+ toolsWrap: s?.settings?.toolsWrap ?? true,
181
182
  visionBridgeEnabled: s?.settings?.visionBridgeEnabled ?? true,
182
183
  visionBridgeModel: s?.settings?.visionBridgeModel ?? null,
183
184
  visionBridgePromptMode: s?.settings?.visionBridgePromptMode === "replace" ? "replace" : "append",
@@ -201,6 +202,7 @@ export class ClientStateStore {
201
202
  terminalBash: settings.terminalBash ?? cur.terminalBash ?? false,
202
203
  terminalBashIdleMs: settings.terminalBashIdleMs ?? cur.terminalBashIdleMs ?? 15_000,
203
204
  thinkingWrap: settings.thinkingWrap ?? cur.thinkingWrap ?? false,
205
+ toolsWrap: settings.toolsWrap ?? cur.toolsWrap ?? true,
204
206
  visionBridgeEnabled: settings.visionBridgeEnabled ?? cur.visionBridgeEnabled ?? true,
205
207
  visionBridgeModel: settings.visionBridgeModel ?? cur.visionBridgeModel ?? null,
206
208
  visionBridgePromptMode: settings.visionBridgePromptMode ??
@@ -179,6 +179,11 @@ const here = dirname(fileURLToPath(import.meta.url)); // <pkg>/dist/server or <p
179
179
  // Resolve the package root robustly: dev runs from <repo>/server (tsx), prod
180
180
  // from <pkg>/dist/server — the ancestor that actually has package.json wins.
181
181
  function resolvePkgRoot() {
182
+ // PI_WEB_PKG_ROOT: Electron 桌面版打包后,server 子进程从 extraResources 目录
183
+ //(process.resourcesPath)加载 web/dist 和 themes。通过这个 env var 告诉
184
+ // server 去哪里找 pkgRoot,避免 resolvePkgRoot 的候选路径找不到 package.json。
185
+ if (process.env.PI_WEB_PKG_ROOT)
186
+ return process.env.PI_WEB_PKG_ROOT;
182
187
  const candidates = [
183
188
  resolve(here, ".."),
184
189
  resolve(here, "..", ".."),
@@ -717,6 +722,7 @@ wss.on("connection", (ws) => {
717
722
  terminalBash: msg.terminalBash,
718
723
  terminalBashIdleMs: msg.terminalBashIdleMs,
719
724
  thinkingWrap: msg.thinkingWrap,
725
+ toolsWrap: msg.toolsWrap,
720
726
  visionBridgeEnabled: msg.visionBridgeEnabled,
721
727
  visionBridgeModel: msg.visionBridgeModel,
722
728
  visionBridgePromptMode: msg.visionBridgePromptMode,
@@ -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 = 9;
11
+ export const PROTOCOL_VERSION = 10;