pi-web-ui 0.8.6 → 0.8.8
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 +397 -414
- package/README.zh-CN.md +359 -359
- package/bin/pi-web-ui.mjs +0 -0
- package/deploy/com.xingshuyin.pi-web-ui.plist +48 -48
- package/deploy/pi-web-ui-task.xml +71 -71
- package/deploy/pi-web-ui.service +31 -31
- package/dist/server/agent-service.js +125 -45
- package/dist/server/index.js +19 -2
- package/package.json +1 -1
- package/web/dist/assets/{index-DicNqNt1.css → index-DBMeSvD4.css} +1 -1
- package/web/dist/assets/{index-BPM4qIYE.js → index-DfP_jZuo.js} +2 -2
- package/web/dist/favicon.svg +8 -8
- package/web/dist/index.html +15 -15
package/bin/pi-web-ui.mjs
CHANGED
|
File without changes
|
|
@@ -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,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>
|
package/deploy/pi-web-ui.service
CHANGED
|
@@ -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
|
|
@@ -126,6 +126,7 @@ const PREVIEW_TEXT_EXTS = new Set([
|
|
|
126
126
|
"json",
|
|
127
127
|
"jsonc",
|
|
128
128
|
"json5",
|
|
129
|
+
"jsonl",
|
|
129
130
|
"md",
|
|
130
131
|
"mdx",
|
|
131
132
|
"markdown",
|
|
@@ -178,6 +179,43 @@ export function previewKind(name) {
|
|
|
178
179
|
return "text";
|
|
179
180
|
return "none";
|
|
180
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Content sniff for the preview: any data that has no NUL bytes and no
|
|
184
|
+
* meaningful control-char ratio is treated as text — so files with unknown
|
|
185
|
+
* or absent extensions (jsonl, .log.1, …) still open as text. NULs catch
|
|
186
|
+
* zip/sqlite/png/… even when the extension claims text.
|
|
187
|
+
*/
|
|
188
|
+
function looksLikeText(buf) {
|
|
189
|
+
if (buf.length === 0)
|
|
190
|
+
return true;
|
|
191
|
+
if (buf.includes(0))
|
|
192
|
+
return false;
|
|
193
|
+
const text = buf.toString("utf8");
|
|
194
|
+
let control = 0;
|
|
195
|
+
for (const ch of text) {
|
|
196
|
+
const c = ch.charCodeAt(0);
|
|
197
|
+
// Keep \t \n \r \f (and \b); everything else < 0x20 is binary-ish.
|
|
198
|
+
if (c < 0x20 && c !== 9 && c !== 10 && c !== 12 && c !== 13)
|
|
199
|
+
control++;
|
|
200
|
+
}
|
|
201
|
+
return control / Math.max(text.length, 1) < 0.02;
|
|
202
|
+
}
|
|
203
|
+
/** First few KB of binary data as a classic hex + ASCII dump (preview only). */
|
|
204
|
+
function hexDump(buf, maxBytes = 4096) {
|
|
205
|
+
const data = buf.subarray(0, Math.min(buf.length, maxBytes));
|
|
206
|
+
const rows = [];
|
|
207
|
+
for (let off = 0; off < data.length; off += 16) {
|
|
208
|
+
const chunk = data.subarray(off, off + 16);
|
|
209
|
+
const hex = [...chunk]
|
|
210
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
211
|
+
.join(" ");
|
|
212
|
+
const ascii = [...chunk]
|
|
213
|
+
.map((b) => (b >= 0x20 && b < 0x7f ? String.fromCharCode(b) : "."))
|
|
214
|
+
.join("");
|
|
215
|
+
rows.push(`${off.toString(16).padStart(8, "0")} ${hex.padEnd(47, " ")} ${ascii}`);
|
|
216
|
+
}
|
|
217
|
+
return rows.join("\n");
|
|
218
|
+
}
|
|
181
219
|
// ---------------------------------------------------------------------------
|
|
182
220
|
// Web UI context adapter — bridges extension UI calls (setWidget/notify) to the
|
|
183
221
|
// browser. Extensions like rpiv-todo render a TUI widget via
|
|
@@ -988,6 +1026,41 @@ export class ClientSession {
|
|
|
988
1026
|
});
|
|
989
1027
|
}
|
|
990
1028
|
}
|
|
1029
|
+
/**
|
|
1030
|
+
* After `npm i -g`, confirm the on-disk package this process serves from
|
|
1031
|
+
* actually changed to the new version and is complete. Windows npm updates
|
|
1032
|
+
* can fail partway (locked files / Defender / npm rollback) and leave the
|
|
1033
|
+
* global install without its bin links — restarting into that is a silent
|
|
1034
|
+
* crash (web/dist missing + `pi-web-ui` no longer on PATH). Returns null
|
|
1035
|
+
* when OK, else a human-readable problem description.
|
|
1036
|
+
*/
|
|
1037
|
+
static verifyGlobalInstall() {
|
|
1038
|
+
try {
|
|
1039
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1040
|
+
const pkgRoot = resolve(here, "..", "..");
|
|
1041
|
+
const pkg = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8"));
|
|
1042
|
+
if (!pkg.version || pkg.version === ClientSession.currentAppVersion()) {
|
|
1043
|
+
return `安装目录版本未变化(${pkg.version ?? "未知"})`;
|
|
1044
|
+
}
|
|
1045
|
+
if (!existsSync(join(pkgRoot, "web", "dist", "index.html"))) {
|
|
1046
|
+
return "web/dist/index.html 缺失(前端产物未安装完整)";
|
|
1047
|
+
}
|
|
1048
|
+
if (!existsSync(join(pkgRoot, "bin", "pi-web-ui.mjs"))) {
|
|
1049
|
+
return "bin/pi-web-ui.mjs 缺失";
|
|
1050
|
+
}
|
|
1051
|
+
if (process.platform === "win32") {
|
|
1052
|
+
const prefix = dirname(process.execPath);
|
|
1053
|
+
const hasShim = existsSync(join(prefix, "pi-web-ui.cmd")) ||
|
|
1054
|
+
existsSync(join(prefix, "pi-web-ui.ps1"));
|
|
1055
|
+
if (!hasShim)
|
|
1056
|
+
return "pi-web-ui 命令入口(bin 链接)未生成";
|
|
1057
|
+
}
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
catch (err) {
|
|
1061
|
+
return `读取安装目录失败:${err.message}`;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
991
1064
|
/** npm i -g pi-web-ui@latest — the new code only takes effect after a restart. */
|
|
992
1065
|
async updateApp() {
|
|
993
1066
|
try {
|
|
@@ -997,34 +1070,50 @@ export class ClientSession {
|
|
|
997
1070
|
text: "正在更新 pi-web-ui(npm i -g pi-web-ui@latest)…",
|
|
998
1071
|
});
|
|
999
1072
|
const { code, out } = await this.runAsync("npm", ["i", "-g", "pi-web-ui@latest"], 180_000);
|
|
1000
|
-
if (code
|
|
1001
|
-
this.pendingRestart = true;
|
|
1073
|
+
if (code !== 0) {
|
|
1002
1074
|
this.emit({
|
|
1003
1075
|
type: "update_result",
|
|
1004
|
-
ok:
|
|
1005
|
-
detail: out.slice(0, 400)
|
|
1076
|
+
ok: false,
|
|
1077
|
+
detail: `npm i 失败(${code ?? "timeout"}):${out.slice(0, 400)}`,
|
|
1006
1078
|
});
|
|
1007
|
-
const autoRestart = this.onUpdateReady?.() ?? false;
|
|
1008
1079
|
this.emit({
|
|
1009
1080
|
type: "notice",
|
|
1010
|
-
level: "
|
|
1011
|
-
text:
|
|
1012
|
-
? "✅ 已更新 pi-web-ui,正在自动重启…"
|
|
1013
|
-
: "✅ 已更新 pi-web-ui,重启服务后生效(pi-web-ui server restart)",
|
|
1081
|
+
level: "error",
|
|
1082
|
+
text: `更新 pi-web-ui 失败(${code ?? "timeout"}):${out.slice(0, 300)}`,
|
|
1014
1083
|
});
|
|
1084
|
+
return;
|
|
1015
1085
|
}
|
|
1016
|
-
|
|
1086
|
+
// npm reported success, but on Windows the replacement can be partial
|
|
1087
|
+
// (locked files, rollback) — restarting into a broken install is a
|
|
1088
|
+
// crash with no hint. Verify before handing over.
|
|
1089
|
+
const problem = ClientSession.verifyGlobalInstall();
|
|
1090
|
+
if (problem) {
|
|
1017
1091
|
this.emit({
|
|
1018
1092
|
type: "update_result",
|
|
1019
1093
|
ok: false,
|
|
1020
|
-
detail: `npm i
|
|
1094
|
+
detail: `npm i 成功但安装不完整(${problem})。请手动执行 npm i -g pi-web-ui@latest 修复后再重启服务。`,
|
|
1021
1095
|
});
|
|
1022
1096
|
this.emit({
|
|
1023
1097
|
type: "notice",
|
|
1024
1098
|
level: "error",
|
|
1025
|
-
text:
|
|
1099
|
+
text: `更新未完整生效(${problem})。请手动执行 npm i -g pi-web-ui@latest 修复`,
|
|
1026
1100
|
});
|
|
1101
|
+
return;
|
|
1027
1102
|
}
|
|
1103
|
+
this.pendingRestart = true;
|
|
1104
|
+
this.emit({
|
|
1105
|
+
type: "update_result",
|
|
1106
|
+
ok: true,
|
|
1107
|
+
detail: out.slice(0, 400),
|
|
1108
|
+
});
|
|
1109
|
+
const autoRestart = this.onUpdateReady?.() ?? false;
|
|
1110
|
+
this.emit({
|
|
1111
|
+
type: "notice",
|
|
1112
|
+
level: "info",
|
|
1113
|
+
text: autoRestart
|
|
1114
|
+
? "✅ 已更新 pi-web-ui,正在自动重启…"
|
|
1115
|
+
: "✅ 已更新 pi-web-ui,重启服务后生效(pi-web-ui server restart)",
|
|
1116
|
+
});
|
|
1028
1117
|
}
|
|
1029
1118
|
catch (err) {
|
|
1030
1119
|
this.emit({
|
|
@@ -2026,21 +2115,6 @@ export class ClientSession {
|
|
|
2026
2115
|
}
|
|
2027
2116
|
const name = relPath.split("/").pop() ?? relPath;
|
|
2028
2117
|
const kind = previewKind(name);
|
|
2029
|
-
// Not previewable (exe, jar, archives, …) — never read or sent.
|
|
2030
|
-
if (kind === "none") {
|
|
2031
|
-
this.emit({
|
|
2032
|
-
type: "file_content",
|
|
2033
|
-
path: rel,
|
|
2034
|
-
name,
|
|
2035
|
-
text: "",
|
|
2036
|
-
truncated: false,
|
|
2037
|
-
binary: true,
|
|
2038
|
-
kind: "none",
|
|
2039
|
-
lines: 0,
|
|
2040
|
-
size: stat.size,
|
|
2041
|
-
});
|
|
2042
|
-
return;
|
|
2043
|
-
}
|
|
2044
2118
|
// Media previews stream over the /api/file HTTP endpoint, so only
|
|
2045
2119
|
// metadata is sent here — the raw bytes never touch the socket.
|
|
2046
2120
|
if (kind === "image" || kind === "video") {
|
|
@@ -2057,38 +2131,41 @@ export class ClientSession {
|
|
|
2057
2131
|
});
|
|
2058
2132
|
return;
|
|
2059
2133
|
}
|
|
2060
|
-
//
|
|
2061
|
-
//
|
|
2134
|
+
// Everything else: read a capped prefix and sniff the content.
|
|
2135
|
+
// Anything that looks like text previews as text regardless of its
|
|
2136
|
+
// extension (jsonl, .log.1, weird suffixes, …); binary content gets
|
|
2137
|
+
// a hex dump of the first few KB instead of being refused.
|
|
2062
2138
|
const handle = await fs.open(abs, "r");
|
|
2063
2139
|
try {
|
|
2064
2140
|
const buf = Buffer.alloc(Math.min(stat.size, MAX_PREVIEW_BYTES));
|
|
2065
2141
|
const { bytesRead } = await handle.read(buf, 0, buf.length, 0);
|
|
2066
2142
|
const data = buf.subarray(0, bytesRead);
|
|
2067
|
-
if (data
|
|
2143
|
+
if (looksLikeText(data)) {
|
|
2068
2144
|
this.emit({
|
|
2069
2145
|
type: "file_content",
|
|
2070
2146
|
path: rel,
|
|
2071
2147
|
name,
|
|
2072
|
-
text: "",
|
|
2073
|
-
truncated:
|
|
2074
|
-
binary:
|
|
2148
|
+
text: data.toString("utf8"),
|
|
2149
|
+
truncated: bytesRead < stat.size,
|
|
2150
|
+
binary: false,
|
|
2075
2151
|
kind: "text",
|
|
2152
|
+
lines: countLines(data),
|
|
2153
|
+
size: stat.size,
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
else {
|
|
2157
|
+
this.emit({
|
|
2158
|
+
type: "file_content",
|
|
2159
|
+
path: rel,
|
|
2160
|
+
name,
|
|
2161
|
+
text: hexDump(data),
|
|
2162
|
+
truncated: bytesRead < stat.size,
|
|
2163
|
+
binary: true,
|
|
2164
|
+
kind: kind === "text" ? "text" : "none",
|
|
2076
2165
|
lines: 0,
|
|
2077
2166
|
size: stat.size,
|
|
2078
2167
|
});
|
|
2079
|
-
return;
|
|
2080
2168
|
}
|
|
2081
|
-
this.emit({
|
|
2082
|
-
type: "file_content",
|
|
2083
|
-
path: rel,
|
|
2084
|
-
name,
|
|
2085
|
-
text: data.toString("utf8"),
|
|
2086
|
-
truncated: bytesRead < stat.size,
|
|
2087
|
-
binary: false,
|
|
2088
|
-
kind: "text",
|
|
2089
|
-
lines: countLines(data),
|
|
2090
|
-
size: stat.size,
|
|
2091
|
-
});
|
|
2092
2169
|
}
|
|
2093
2170
|
finally {
|
|
2094
2171
|
await handle.close();
|
|
@@ -2212,6 +2289,9 @@ export class ClientSession {
|
|
|
2212
2289
|
conv.runtime = newRuntime;
|
|
2213
2290
|
conv.session = newRuntime.session;
|
|
2214
2291
|
conv.cwd = abs;
|
|
2292
|
+
// The rebuilt runtime resumed the new project's most recent session —
|
|
2293
|
+
// refresh the title too, or the conversation list shows the old name.
|
|
2294
|
+
conv.title = conversationTitle(newRuntime.session);
|
|
2215
2295
|
this.cwd = abs;
|
|
2216
2296
|
// Remember the new workspace (restore target + recent-project entry).
|
|
2217
2297
|
this.stateStore.remember(this.clientId, abs);
|
package/dist/server/index.js
CHANGED
|
@@ -89,13 +89,31 @@ app.get("/api/file", async (req, res) => {
|
|
|
89
89
|
// from the repo root. In dev, Vite serves the UI on :5173 and proxies /ws.
|
|
90
90
|
const here = dirname(fileURLToPath(import.meta.url)); // <pkg>/dist/server or <pkg>/server
|
|
91
91
|
const pkgRoot = resolve(here, "..", "..");
|
|
92
|
+
/** Set in the env of the replacement child spawned by a self-update restart. */
|
|
93
|
+
const RESTART_CHILD_ENV = "PI_WEB_RESTART_CHILD";
|
|
92
94
|
const webDist = join(pkgRoot, "web", "dist");
|
|
93
95
|
if (existsSync(webDist)) {
|
|
94
96
|
app.use(express.static(webDist));
|
|
95
97
|
app.get(/^\/(?!api\/|ws).*/, (_req, res) => {
|
|
96
|
-
|
|
98
|
+
// Callback form: a failed stat here (npm i -g is mid-replacement of the
|
|
99
|
+
// package dir) responds 503 instead of crashing the request pipeline
|
|
100
|
+
// with an unhandled ENOENT stack trace.
|
|
101
|
+
res.sendFile(join(webDist, "index.html"), (err) => {
|
|
102
|
+
if (err && !res.headersSent) {
|
|
103
|
+
res.status(503).send("正在更新 pi-web-ui,请稍后刷新…");
|
|
104
|
+
}
|
|
105
|
+
});
|
|
97
106
|
});
|
|
98
107
|
}
|
|
108
|
+
else if (process.env[RESTART_CHILD_ENV]) {
|
|
109
|
+
// Auto-restart replacement of a self-update whose npm install did not
|
|
110
|
+
// complete (Windows: locked files / rollback can leave the global package
|
|
111
|
+
// without web/dist). Fail loudly with a repair hint instead of serving a
|
|
112
|
+
// UI-less 404 with no explanation.
|
|
113
|
+
console.error("✖ 更新后的安装不完整(缺少 web/dist/index.html)。\n" +
|
|
114
|
+
" 请手动执行 npm i -g pi-web-ui@latest 修复后重新启动。");
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
99
117
|
const httpServer = createServer(app);
|
|
100
118
|
const wss = new WebSocketServer({ server: httpServer, path: "/ws" });
|
|
101
119
|
// Heartbeat: lets clients detect half-open connections (server killed without
|
|
@@ -120,7 +138,6 @@ join(DATA_DIR, "client-state.json"));
|
|
|
120
138
|
// foreground runs get a replacement child that waits for our port to free.
|
|
121
139
|
// Docker containers can't self-restart (the orchestrator owns that), so they
|
|
122
140
|
// keep the manual-restart notice.
|
|
123
|
-
const RESTART_CHILD_ENV = "PI_WEB_RESTART_CHILD";
|
|
124
141
|
function scheduleUpdateRestart() {
|
|
125
142
|
const isLaunchd = process.platform === "darwin" && process.ppid === 1;
|
|
126
143
|
const isSystemd = process.platform === "linux" && !!process.env.INVOCATION_ID;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-ui",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.8",
|
|
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
|
"type": "module",
|