pi-web-ui 0.8.8 → 0.9.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/LICENSE +21 -21
- package/README.md +414 -397
- 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 +121 -31
- package/package.json +2 -2
- package/web/dist/assets/index-8BJsbrQQ.css +41 -0
- package/web/dist/assets/{index-DfP_jZuo.js → index-DTwK1ZcB.js} +1 -1
- package/web/dist/favicon.svg +8 -8
- package/web/dist/index.html +15 -15
- package/web/dist/assets/index-DBMeSvD4.css +0 -41
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
|
|
@@ -424,6 +424,8 @@ export class WebUIContext {
|
|
|
424
424
|
function sanitizeId(id) {
|
|
425
425
|
return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80) || "anon";
|
|
426
426
|
}
|
|
427
|
+
const IS_WIN32 = process.platform === "win32";
|
|
428
|
+
// mac/linux: hide build & dependency noise (original behavior).
|
|
427
429
|
const IGNORED_ENTRIES = new Set([
|
|
428
430
|
"node_modules",
|
|
429
431
|
".git",
|
|
@@ -441,6 +443,22 @@ const IGNORED_ENTRIES = new Set([
|
|
|
441
443
|
".DS_Store",
|
|
442
444
|
"Thumbs.db",
|
|
443
445
|
]);
|
|
446
|
+
// Windows: the file tree is the primary way to navigate a project, so only
|
|
447
|
+
// hide what would flood or destabilize the panel (dependency trees, VCS
|
|
448
|
+
// internals, session data) plus pure junk. Build output (dist/.next/…) and
|
|
449
|
+
// local env dirs (venv/__pycache__/…) stay visible — "所有文件可查看".
|
|
450
|
+
const IGNORED_ENTRIES_WIN = new Set([
|
|
451
|
+
"node_modules",
|
|
452
|
+
".git",
|
|
453
|
+
".pi-web",
|
|
454
|
+
".DS_Store",
|
|
455
|
+
"Thumbs.db",
|
|
456
|
+
"desktop.ini",
|
|
457
|
+
]);
|
|
458
|
+
/** The ignore set for the current platform — keeps win/posix lists separate. */
|
|
459
|
+
function ignoredEntries() {
|
|
460
|
+
return IS_WIN32 ? IGNORED_ENTRIES_WIN : IGNORED_ENTRIES;
|
|
461
|
+
}
|
|
444
462
|
function countLines(buf) {
|
|
445
463
|
if (buf.length === 0)
|
|
446
464
|
return 0;
|
|
@@ -474,10 +492,81 @@ function extractPartialText(partial) {
|
|
|
474
492
|
*/
|
|
475
493
|
export function workspacePath(root, raw) {
|
|
476
494
|
const abs = resolve(root, raw);
|
|
477
|
-
const
|
|
478
|
-
if (
|
|
495
|
+
const rawRel = relative(root, abs);
|
|
496
|
+
if (rawRel.startsWith("..") || rawRel.includes(`${sep}..`))
|
|
479
497
|
return null;
|
|
480
|
-
|
|
498
|
+
// Normalize to forward slashes: the wire protocol and the frontend always
|
|
499
|
+
// use "/", but relative() returns "\\" on Windows.
|
|
500
|
+
return { abs, rel: rawRel.split(sep).join("/") };
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Read a directory for the file panel. The two platforms intentionally use
|
|
504
|
+
* different strategies — do NOT unify them:
|
|
505
|
+
*
|
|
506
|
+
* darwin/linux (posix): original behavior — hide build/dependency noise,
|
|
507
|
+
* small cap, hard error notice when the directory itself is unreadable.
|
|
508
|
+
*
|
|
509
|
+
* win32: stability and completeness first, preview second. ACL-protected
|
|
510
|
+
* system dirs (C:\$Recycle.Bin, Program Files internals, OneDrive placeholders)
|
|
511
|
+
* throw EPERM/EACCES on open — that must not kill the panel, so it degrades
|
|
512
|
+
* to an empty listing plus a warning. Directory symlinks/junctions are
|
|
513
|
+
* followed so mklink /D folders stay navigable; broken links still show as
|
|
514
|
+
* files instead of vanishing. The cap is 4x posix and truncation is reported
|
|
515
|
+
* via `truncated` instead of happening silently.
|
|
516
|
+
*/
|
|
517
|
+
async function readDirForUI(abs, rel) {
|
|
518
|
+
const fs = await import("node:fs/promises");
|
|
519
|
+
const ignored = ignoredEntries();
|
|
520
|
+
const MAX = IS_WIN32 ? 2000 : 500;
|
|
521
|
+
let dirents;
|
|
522
|
+
try {
|
|
523
|
+
dirents = await fs.readdir(abs, { withFileTypes: true });
|
|
524
|
+
}
|
|
525
|
+
catch (err) {
|
|
526
|
+
if (!IS_WIN32)
|
|
527
|
+
throw err;
|
|
528
|
+
// Windows ACL-protected/system dirs throw EPERM/EACCES on open —
|
|
529
|
+
// degrade to an empty listing; listFiles turns this into a warning.
|
|
530
|
+
return { entries: [], truncated: false, error: err.message };
|
|
531
|
+
}
|
|
532
|
+
const out = [];
|
|
533
|
+
for (const d of dirents) {
|
|
534
|
+
if (ignored.has(d.name))
|
|
535
|
+
continue;
|
|
536
|
+
let type;
|
|
537
|
+
if (IS_WIN32 && d.isSymbolicLink()) {
|
|
538
|
+
// mklink /D symlinks and junctions are reparse points — libuv
|
|
539
|
+
// classifies them as links, so isDirectory() is false. Follow the
|
|
540
|
+
// target so folder links stay navigable; broken links still show.
|
|
541
|
+
try {
|
|
542
|
+
const st = await fs.stat(join(abs, d.name));
|
|
543
|
+
type = st.isDirectory() ? "dir" : "file";
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
type = "file";
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
type = d.isDirectory() ? "dir" : "file";
|
|
551
|
+
}
|
|
552
|
+
const entry = {
|
|
553
|
+
name: d.name,
|
|
554
|
+
path: rel === "" ? d.name : `${rel}/${d.name}`,
|
|
555
|
+
type,
|
|
556
|
+
};
|
|
557
|
+
if (type === "file")
|
|
558
|
+
entry.kind = previewKind(d.name);
|
|
559
|
+
out.push(entry);
|
|
560
|
+
}
|
|
561
|
+
out.sort((a, b) => a.type === b.type
|
|
562
|
+
? a.name.localeCompare(b.name)
|
|
563
|
+
: a.type === "dir"
|
|
564
|
+
? -1
|
|
565
|
+
: 1);
|
|
566
|
+
const truncated = out.length > MAX;
|
|
567
|
+
if (truncated)
|
|
568
|
+
out.length = MAX;
|
|
569
|
+
return { entries: out, truncated };
|
|
481
570
|
}
|
|
482
571
|
/**
|
|
483
572
|
* Persists which workspace each browser client last used + which workspaces it
|
|
@@ -1529,8 +1618,8 @@ export class ClientSession {
|
|
|
1529
1618
|
const MAX_LINES_READ_BYTES = 2 * 1024 * 1024;
|
|
1530
1619
|
for (const att of attachments) {
|
|
1531
1620
|
const abs = resolve(root, att.path);
|
|
1532
|
-
const
|
|
1533
|
-
if (
|
|
1621
|
+
const rawRel = relative(root, abs);
|
|
1622
|
+
if (rawRel.startsWith("..") || rawRel.includes(`${sep}..`)) {
|
|
1534
1623
|
this.emit({
|
|
1535
1624
|
type: "notice",
|
|
1536
1625
|
level: "warning",
|
|
@@ -1538,6 +1627,9 @@ export class ClientSession {
|
|
|
1538
1627
|
});
|
|
1539
1628
|
continue;
|
|
1540
1629
|
}
|
|
1630
|
+
// Normalize to forward slashes (relative() returns "\\" on Windows);
|
|
1631
|
+
// <file path> and details.path must use the wire format.
|
|
1632
|
+
const rel = rawRel.split(sep).join("/");
|
|
1541
1633
|
let stat;
|
|
1542
1634
|
try {
|
|
1543
1635
|
stat = await fs.stat(abs);
|
|
@@ -1550,7 +1642,7 @@ export class ClientSession {
|
|
|
1550
1642
|
});
|
|
1551
1643
|
continue;
|
|
1552
1644
|
}
|
|
1553
|
-
const name = att.path.split(
|
|
1645
|
+
const name = att.path.split(/[\\/]/).pop() ?? att.path;
|
|
1554
1646
|
// Folders can't be inlined — always a path reference the model browses
|
|
1555
1647
|
// on demand with its own tools (ls/read).
|
|
1556
1648
|
if (stat.isDirectory()) {
|
|
@@ -2038,12 +2130,11 @@ export class ClientSession {
|
|
|
2038
2130
|
/** List a workspace directory (relative to the configured cwd). */
|
|
2039
2131
|
async listFiles(relPath) {
|
|
2040
2132
|
try {
|
|
2041
|
-
const fs = await import("node:fs/promises");
|
|
2042
2133
|
const { resolve, sep, relative } = await import("node:path");
|
|
2043
2134
|
const root = resolve(this.cwd);
|
|
2044
2135
|
const target = relPath ? resolve(root, relPath) : root;
|
|
2045
|
-
const
|
|
2046
|
-
if (
|
|
2136
|
+
const rawRel = relative(root, target);
|
|
2137
|
+
if (rawRel.startsWith("..") || rawRel.includes(`${sep}..`)) {
|
|
2047
2138
|
this.emit({
|
|
2048
2139
|
type: "notice",
|
|
2049
2140
|
level: "warning",
|
|
@@ -2051,25 +2142,19 @@ export class ClientSession {
|
|
|
2051
2142
|
});
|
|
2052
2143
|
return;
|
|
2053
2144
|
}
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
}
|
|
2067
|
-
.sort((a, b) => a.type === b.type
|
|
2068
|
-
? a.name.localeCompare(b.name)
|
|
2069
|
-
: a.type === "dir"
|
|
2070
|
-
? -1
|
|
2071
|
-
: 1)
|
|
2072
|
-
.slice(0, 500);
|
|
2145
|
+
// Normalize to forward slashes: the wire protocol and the frontend
|
|
2146
|
+
// always use "/", but relative() returns "\\" on Windows.
|
|
2147
|
+
const rel = rawRel.split(sep).join("/");
|
|
2148
|
+
const { entries, truncated, error } = await readDirForUI(target, rel);
|
|
2149
|
+
if (error) {
|
|
2150
|
+
// Windows-only: unreadable system dirs degrade to an empty list
|
|
2151
|
+
// with a warning instead of a hard error — the panel stays usable.
|
|
2152
|
+
this.emit({
|
|
2153
|
+
type: "notice",
|
|
2154
|
+
level: "warning",
|
|
2155
|
+
text: `目录不可读:${error}`,
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2073
2158
|
this.emit({
|
|
2074
2159
|
type: "files",
|
|
2075
2160
|
path: rel === "" ? "" : rel,
|
|
@@ -2079,6 +2164,7 @@ export class ClientSession {
|
|
|
2079
2164
|
? rel.slice(0, rel.lastIndexOf("/"))
|
|
2080
2165
|
: "",
|
|
2081
2166
|
entries,
|
|
2167
|
+
truncated,
|
|
2082
2168
|
});
|
|
2083
2169
|
}
|
|
2084
2170
|
catch (err) {
|
|
@@ -2113,7 +2199,7 @@ export class ClientSession {
|
|
|
2113
2199
|
});
|
|
2114
2200
|
return;
|
|
2115
2201
|
}
|
|
2116
|
-
const name = relPath.split(
|
|
2202
|
+
const name = relPath.split(/[\\/]/).pop() ?? relPath;
|
|
2117
2203
|
const kind = previewKind(name);
|
|
2118
2204
|
// Media previews stream over the /api/file HTTP endpoint, so only
|
|
2119
2205
|
// metadata is sent here — the raw bytes never touch the socket.
|
|
@@ -2232,10 +2318,14 @@ export class ClientSession {
|
|
|
2232
2318
|
return;
|
|
2233
2319
|
}
|
|
2234
2320
|
const completions = dirents
|
|
2235
|
-
.filter((d) => d.name.startsWith(prefix) && !
|
|
2321
|
+
.filter((d) => d.name.startsWith(prefix) && !ignoredEntries().has(d.name))
|
|
2236
2322
|
.map((d) => ({
|
|
2237
2323
|
name: d.name,
|
|
2238
|
-
|
|
2324
|
+
// Windows users type backslashes — normalize the completion to the
|
|
2325
|
+
// wire format ("/") so the picked path round-trips cleanly.
|
|
2326
|
+
path: IS_WIN32
|
|
2327
|
+
? join(dirPart, d.name).split(sep).join("/")
|
|
2328
|
+
: dirPart + d.name,
|
|
2239
2329
|
type: (d.isDirectory() ? "dir" : "file"),
|
|
2240
2330
|
}))
|
|
2241
2331
|
.sort((a, b) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"scripts": {
|
|
36
36
|
"prepublishOnly": "npm run build",
|
|
37
37
|
"dev": "concurrently -k -n server,web -c blue,green \"npm:dev:server\" \"npm:dev:web\"",
|
|
38
|
-
"dev:server": "
|
|
38
|
+
"dev:server": "node --watch --import tsx server/index.ts",
|
|
39
39
|
"dev:web": "vite --config web/vite.config.ts",
|
|
40
40
|
"build": "npm run build:web && npm run build:server",
|
|
41
41
|
"build:web": "vite build --config web/vite.config.ts",
|