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/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>
@@ -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 rel = relative(root, abs);
478
- if (rel.startsWith("..") || rel.includes(`${sep}..`))
495
+ const rawRel = relative(root, abs);
496
+ if (rawRel.startsWith("..") || rawRel.includes(`${sep}..`))
479
497
  return null;
480
- return { abs, rel };
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 rel = relative(root, abs);
1533
- if (rel.startsWith("..") || rel.includes(`${sep}..`)) {
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("/").pop() ?? att.path;
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 rel = relative(root, target);
2046
- if (rel.startsWith("..") || rel.includes(`${sep}..`)) {
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
- const dirents = await fs.readdir(target, { withFileTypes: true });
2055
- const entries = dirents
2056
- .filter((d) => !IGNORED_ENTRIES.has(d.name))
2057
- .map((d) => {
2058
- const entry = {
2059
- name: d.name,
2060
- path: rel === "" ? d.name : `${rel}/${d.name}`,
2061
- type: (d.isDirectory() ? "dir" : "file"),
2062
- };
2063
- if (!d.isDirectory())
2064
- entry.kind = previewKind(d.name);
2065
- return entry;
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("/").pop() ?? relPath;
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) && !IGNORED_ENTRIES.has(d.name))
2321
+ .filter((d) => d.name.startsWith(prefix) && !ignoredEntries().has(d.name))
2236
2322
  .map((d) => ({
2237
2323
  name: d.name,
2238
- path: dirPart + d.name,
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.8.8",
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": "tsx watch server/index.ts",
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",