pi-web-ui 0.8.9 → 0.9.1
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 +147 -31
- package/package.json +2 -2
- package/web/dist/assets/index-8BJsbrQQ.css +41 -0
- package/web/dist/assets/{index-BCY0KFq6.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-twSA97ef.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()) {
|
|
@@ -1771,6 +1863,32 @@ export class ClientSession {
|
|
|
1771
1863
|
this.flushSnapshot();
|
|
1772
1864
|
}
|
|
1773
1865
|
async newChat() {
|
|
1866
|
+
// Reuse an already-open blank conversation instead of piling up new ones
|
|
1867
|
+
// on every click: if the active chat has no messages it IS the new chat
|
|
1868
|
+
// (focus already on it); otherwise switch to the first blank one.
|
|
1869
|
+
const isBlank = (c) => {
|
|
1870
|
+
try {
|
|
1871
|
+
return c.session.getSessionStats().totalMessages === 0;
|
|
1872
|
+
}
|
|
1873
|
+
catch {
|
|
1874
|
+
// session being replaced — treat as used so we don't switch onto it
|
|
1875
|
+
return false;
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
const active = this.conv;
|
|
1879
|
+
if (active && isBlank(active)) {
|
|
1880
|
+
this.flushSnapshot();
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
for (const conv of this.convs.values()) {
|
|
1884
|
+
if (conv.id === this.activeId)
|
|
1885
|
+
continue;
|
|
1886
|
+
if (isBlank(conv)) {
|
|
1887
|
+
await this.switchConversation(conv.id);
|
|
1888
|
+
this.flushSnapshot();
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1774
1892
|
if (this.convs.size >= MAX_OPEN_CONVERSATIONS) {
|
|
1775
1893
|
this.emit({
|
|
1776
1894
|
type: "notice",
|
|
@@ -2038,12 +2156,11 @@ export class ClientSession {
|
|
|
2038
2156
|
/** List a workspace directory (relative to the configured cwd). */
|
|
2039
2157
|
async listFiles(relPath) {
|
|
2040
2158
|
try {
|
|
2041
|
-
const fs = await import("node:fs/promises");
|
|
2042
2159
|
const { resolve, sep, relative } = await import("node:path");
|
|
2043
2160
|
const root = resolve(this.cwd);
|
|
2044
2161
|
const target = relPath ? resolve(root, relPath) : root;
|
|
2045
|
-
const
|
|
2046
|
-
if (
|
|
2162
|
+
const rawRel = relative(root, target);
|
|
2163
|
+
if (rawRel.startsWith("..") || rawRel.includes(`${sep}..`)) {
|
|
2047
2164
|
this.emit({
|
|
2048
2165
|
type: "notice",
|
|
2049
2166
|
level: "warning",
|
|
@@ -2051,25 +2168,19 @@ export class ClientSession {
|
|
|
2051
2168
|
});
|
|
2052
2169
|
return;
|
|
2053
2170
|
}
|
|
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);
|
|
2171
|
+
// Normalize to forward slashes: the wire protocol and the frontend
|
|
2172
|
+
// always use "/", but relative() returns "\\" on Windows.
|
|
2173
|
+
const rel = rawRel.split(sep).join("/");
|
|
2174
|
+
const { entries, truncated, error } = await readDirForUI(target, rel);
|
|
2175
|
+
if (error) {
|
|
2176
|
+
// Windows-only: unreadable system dirs degrade to an empty list
|
|
2177
|
+
// with a warning instead of a hard error — the panel stays usable.
|
|
2178
|
+
this.emit({
|
|
2179
|
+
type: "notice",
|
|
2180
|
+
level: "warning",
|
|
2181
|
+
text: `目录不可读:${error}`,
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2073
2184
|
this.emit({
|
|
2074
2185
|
type: "files",
|
|
2075
2186
|
path: rel === "" ? "" : rel,
|
|
@@ -2079,6 +2190,7 @@ export class ClientSession {
|
|
|
2079
2190
|
? rel.slice(0, rel.lastIndexOf("/"))
|
|
2080
2191
|
: "",
|
|
2081
2192
|
entries,
|
|
2193
|
+
truncated,
|
|
2082
2194
|
});
|
|
2083
2195
|
}
|
|
2084
2196
|
catch (err) {
|
|
@@ -2113,7 +2225,7 @@ export class ClientSession {
|
|
|
2113
2225
|
});
|
|
2114
2226
|
return;
|
|
2115
2227
|
}
|
|
2116
|
-
const name = relPath.split(
|
|
2228
|
+
const name = relPath.split(/[\\/]/).pop() ?? relPath;
|
|
2117
2229
|
const kind = previewKind(name);
|
|
2118
2230
|
// Media previews stream over the /api/file HTTP endpoint, so only
|
|
2119
2231
|
// metadata is sent here — the raw bytes never touch the socket.
|
|
@@ -2232,10 +2344,14 @@ export class ClientSession {
|
|
|
2232
2344
|
return;
|
|
2233
2345
|
}
|
|
2234
2346
|
const completions = dirents
|
|
2235
|
-
.filter((d) => d.name.startsWith(prefix) && !
|
|
2347
|
+
.filter((d) => d.name.startsWith(prefix) && !ignoredEntries().has(d.name))
|
|
2236
2348
|
.map((d) => ({
|
|
2237
2349
|
name: d.name,
|
|
2238
|
-
|
|
2350
|
+
// Windows users type backslashes — normalize the completion to the
|
|
2351
|
+
// wire format ("/") so the picked path round-trips cleanly.
|
|
2352
|
+
path: IS_WIN32
|
|
2353
|
+
? join(dirPart, d.name).split(sep).join("/")
|
|
2354
|
+
: dirPart + d.name,
|
|
2239
2355
|
type: (d.isDirectory() ? "dir" : "file"),
|
|
2240
2356
|
}))
|
|
2241
2357
|
.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.1",
|
|
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",
|