pi-web-ui 0.13.1 → 0.14.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 CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 xing-shuyin
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xing-shuyin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.zh-CN.md CHANGED
@@ -157,13 +157,13 @@ pi-web-ui server uninstall # 彻底移除服务
157
157
 
158
158
  ## 配置
159
159
 
160
- | 环境变量 | 默认值 | 说明 |
161
- | --- | --- | --- |
162
- | `PORT` | `8787` | HTTP/WebSocket 端口 |
163
- | `PI_WEB_CWD` | 服务端 cwd | 智能体操作的工作区目录(读/编辑/bash/写) |
164
- | `PI_WEB_DATA_DIR` | `<cwd>/.pi-web` | 每客户端会话目录的存放位置 |
165
- | `PI_WEB_INLINE_FILE_MAX` | `12288` (12KB) | 小于等于该大小的文本附件直接内联进模型上下文;更大的文件以路径引用方式传入,模型按需用 read 工具读取(小改动省 token) |
166
- | `PI_CODING_AGENT_DIR` | `~/.pi/agent` | pi 配置目录(auth.json、models.json、skills、extensions) |
160
+ | 环境变量 | 默认值 | 说明 |
161
+ | -------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------- |
162
+ | `PORT` | `8787` | HTTP/WebSocket 端口 |
163
+ | `PI_WEB_CWD` | 服务端 cwd | 智能体操作的工作区目录(读/编辑/bash/写) |
164
+ | `PI_WEB_DATA_DIR` | `<cwd>/.pi-web` | 每客户端会话目录的存放位置 |
165
+ | `PI_WEB_INLINE_FILE_MAX` | `12288` (12KB) | 小于等于该大小的文本附件直接内联进模型上下文;更大的文件以路径引用方式传入,模型按需用 read 工具读取(小改动省 token) |
166
+ | `PI_CODING_AGENT_DIR` | `~/.pi/agent` | pi 配置目录(auth.json、models.json、skills、extensions) |
167
167
 
168
168
  示例——让智能体面向某个项目:
169
169
 
@@ -265,14 +265,14 @@ server/agent-service.ts 每客户端 ClientSession:
265
265
 
266
266
  ## 脚本
267
267
 
268
- | 脚本 | 作用 |
269
- | --- | --- |
270
- | `npm run dev` | 服务端(tsx watch)+ Vite dev server + WS 代理 |
271
- | `npm run build` | 类型检查 + 构建前端和服务端 |
272
- | `npm start` | 运行生产服务端(提供 `web/dist`) |
273
- | `npm run typecheck` | 双端 `tsc --noEmit` |
274
- | `node terminal-smoke-test.mjs` | WS 层终端/命令协议测试(先 build) |
275
- | `node terminal-browser-test.mjs` | 终端视图的无头浏览器 E2E(先 build) |
268
+ | 脚本 | 作用 |
269
+ | ---------------------------------- | ---------------------------------------------- |
270
+ | `npm run dev` | 服务端(tsx watch)+ Vite dev server + WS 代理 |
271
+ | `npm run build` | 类型检查 + 构建前端和服务端 |
272
+ | `npm start` | 运行生产服务端(提供`web/dist`) |
273
+ | `npm run typecheck` | 双端`tsc --noEmit` |
274
+ | `node terminal-smoke-test.mjs` | WS 层终端/命令协议测试(先 build) |
275
+ | `node terminal-browser-test.mjs` | 终端视图的无头浏览器 E2E(先 build) |
276
276
 
277
277
  ## 部署与开机自启
278
278
 
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
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * AgentService — wraps the pi SDK (@earendil-works/pi-coding-agent) for the web
3
3
  * frontend. Each browser client (identified by a persistent clientId) gets its
4
- * own AgentSessionRuntime with a private session directory, so multiple users /
5
- * tabs never share a transcript file.
4
+ * own AgentSessionRuntime, but sessions live in the SDK default per-project
5
+ * directory (<agentDir>/sessions/--<cwd>--/) the same transcript files the
6
+ * pi CLI/TUI use — so every conversation of a folder shows up everywhere.
6
7
  *
7
8
  * Streaming model: the SDK emits AgentSessionEvents; we forward lightweight
8
9
  * `tool_delta` messages for live tool output and schedule throttled full-state
@@ -446,10 +447,6 @@ export class WebUIContext {
446
447
  this.pendingDialogs.clear();
447
448
  }
448
449
  }
449
- /** Sanitize a clientId (UUID) for use as a directory name. */
450
- function sanitizeId(id) {
451
- return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80) || "anon";
452
- }
453
450
  const IS_WIN32 = process.platform === "win32";
454
451
  // mac/linux: hide build & dependency noise (original behavior).
455
452
  const IGNORED_ENTRIES = new Set([
@@ -683,9 +680,6 @@ function conversationTitle(session) {
683
680
  export class ClientSession {
684
681
  clientId;
685
682
  cwd;
686
- /** Absolute per-client session directory.
687
- /** Absolute per-client session directory. */
688
- sessionDir;
689
683
  /** pi config dir (auth/models/skills). */
690
684
  agentDir;
691
685
  /** Persisted per-client UI state (last workspace + recent projects). */
@@ -734,22 +728,22 @@ export class ClientSession {
734
728
  disposed = false;
735
729
  /** pi-config readiness check, cached briefly so 60ms snapshots don't hit disk. */
736
730
  piCheckCache = null;
737
- constructor(clientId, cwd, sessionDir, agentDir, stateStore) {
731
+ constructor(clientId, cwd, agentDir, stateStore) {
738
732
  this.clientId = clientId;
739
733
  this.cwd = cwd;
740
- this.sessionDir = sessionDir;
741
734
  this.agentDir = agentDir;
742
735
  this.stateStore = stateStore;
743
736
  }
744
- static async create(clientId, cwd, sessionDir, stateStore) {
737
+ static async create(clientId, cwd, stateStore) {
745
738
  const agentDir = process.env.PI_CODING_AGENT_DIR ?? getAgentDir();
746
- const cs = new ClientSession(clientId, cwd, sessionDir, agentDir, stateStore);
739
+ const cs = new ClientSession(clientId, cwd, agentDir, stateStore);
747
740
  const runtime = await createAgentSessionRuntime(cs.makeRuntimeFactory(), {
748
741
  cwd,
749
742
  agentDir,
750
- // Resume the most recent session for this client's private session dir,
751
- // or start a fresh one on first visit.
752
- sessionManager: SessionManager.continueRecent(cwd, sessionDir),
743
+ // Resume the most recent session for this project the SDK default
744
+ // per-project dir (<agentDir>/sessions/--<cwd>--/, shared with the
745
+ // pi CLI/TUI) — or start a fresh one on first visit.
746
+ sessionManager: SessionManager.continueRecent(cwd),
753
747
  });
754
748
  // First conversation = the resumed session; it also seeds the shared
755
749
  // ModelRuntime that every later conversation reuses.
@@ -809,6 +803,7 @@ export class ClientSession {
809
803
  lastMessagesArray: [],
810
804
  queueSteering: 0,
811
805
  queueFollowUp: 0,
806
+ toolStartTimes: new Map(),
812
807
  };
813
808
  }
814
809
  /** Add a socket to this client's broadcast set; flushes buffered startup notices. */
@@ -883,6 +878,52 @@ export class ClientSession {
883
878
  }
884
879
  break;
885
880
  }
881
+ case "tool_execution_start": {
882
+ // Record the moment the tool actually starts so tool_status can
883
+ // report real execution time (vs. time spent waiting on the model).
884
+ conv.toolStartTimes.set(event.toolCallId, Date.now());
885
+ break;
886
+ }
887
+ case "tool_execution_end": {
888
+ const startedAt = conv.toolStartTimes.get(event.toolCallId);
889
+ conv.toolStartTimes.delete(event.toolCallId);
890
+ const durationMs = startedAt !== undefined ? Date.now() - startedAt : undefined;
891
+ // The bash tool does not put its exit code in result.details — on
892
+ // failure it throws "Command exited with code N" and the agent
893
+ // wraps that into the error result text. Try details first (future
894
+ // tools / SDK changes), then parse the error text.
895
+ const details = event.result?.details;
896
+ let exitCode;
897
+ if (typeof details === "object" &&
898
+ details !== null &&
899
+ typeof details.exitCode === "number") {
900
+ exitCode = details.exitCode;
901
+ }
902
+ else if (event.isError) {
903
+ const content = event.result?.content;
904
+ const text = Array.isArray(content)
905
+ ? content
906
+ .map((c) => (typeof c === "object" &&
907
+ c !== null &&
908
+ c.type === "text")
909
+ ? (c.text ?? "")
910
+ : "")
911
+ .join("\n")
912
+ : "";
913
+ const m = text.match(/exited with code (\d+)/);
914
+ if (m)
915
+ exitCode = Number(m[1]);
916
+ }
917
+ this.emit({
918
+ type: "tool_status",
919
+ toolCallId: event.toolCallId,
920
+ toolName: event.toolName,
921
+ isError: event.isError,
922
+ exitCode,
923
+ durationMs,
924
+ });
925
+ break;
926
+ }
886
927
  case "tool_execution_update": {
887
928
  const text = extractPartialText(event.partialResult);
888
929
  if (text) {
@@ -2100,7 +2141,7 @@ export class ClientSession {
2100
2141
  const runtime = await createAgentSessionRuntime(this.makeRuntimeFactory(), {
2101
2142
  cwd: this.cwd,
2102
2143
  agentDir: this.agentDir,
2103
- sessionManager: SessionManager.create(this.cwd, this.sessionDir),
2144
+ sessionManager: SessionManager.create(this.cwd),
2104
2145
  });
2105
2146
  const conv = this.makeConversation(runtime);
2106
2147
  this.convs.set(conv.id, conv);
@@ -2206,23 +2247,12 @@ export class ClientSession {
2206
2247
  }
2207
2248
  async pushSessions() {
2208
2249
  try {
2209
- const { resolve } = await import("node:path");
2210
- // The pi CLI/TUI keeps sessions in <agentDir>/sessions/--<cwd-sanitized>--
2211
- // (encoded per working directory). List those too, so the conversation
2212
- // panel shows every conversation of the current folder — not just the
2213
- // ones created in this web UI.
2214
- const safePath = `--${resolve(this.cwd)
2215
- .replace(/^[/\\]/, "")
2216
- .replace(/[/\\:]/g, "-")}--`;
2217
- const tuiSessionDir = join(this.agentDir, "sessions", safePath);
2218
- const [webInfos, tuiInfos] = await Promise.all([
2219
- SessionManager.list(this.cwd, this.sessionDir),
2220
- existsSync(tuiSessionDir)
2221
- ? SessionManager.list(this.cwd, tuiSessionDir).catch(() => [])
2222
- : Promise.resolve([]),
2223
- ]);
2250
+ // Sessions live in the SDK default per-project dir
2251
+ // (<agentDir>/sessions/--<cwd>--/), the same files the pi CLI/TUI
2252
+ // use one listing covers every conversation of the current folder.
2253
+ const infos = await SessionManager.list(this.cwd);
2224
2254
  const sessions = new Map();
2225
- for (const s of webInfos) {
2255
+ for (const s of infos) {
2226
2256
  sessions.set(s.path, {
2227
2257
  path: s.path,
2228
2258
  name: s.name,
@@ -2232,16 +2262,6 @@ export class ClientSession {
2232
2262
  source: "web",
2233
2263
  });
2234
2264
  }
2235
- for (const s of tuiInfos) {
2236
- sessions.set(s.path, {
2237
- path: s.path,
2238
- name: s.name,
2239
- firstMessage: s.firstMessage,
2240
- messageCount: s.messageCount,
2241
- modified: s.modified.getTime(),
2242
- source: "tui",
2243
- });
2244
- }
2245
2265
  const sorted = [...sessions.values()].sort((a, b) => b.modified - a.modified);
2246
2266
  this.emit({ type: "sessions", sessions: sorted });
2247
2267
  }
@@ -2366,7 +2386,7 @@ export class ClientSession {
2366
2386
  const map = new Map();
2367
2387
  for (const p of saved.projects)
2368
2388
  map.set(p.path, p.lastUsed);
2369
- const all = await SessionManager.listAll(this.sessionDir);
2389
+ const all = await SessionManager.listAll();
2370
2390
  for (const s of all) {
2371
2391
  if (s.cwd) {
2372
2392
  const t = s.modified.getTime();
@@ -2655,7 +2675,7 @@ export class ClientSession {
2655
2675
  const newRuntime = await createAgentSessionRuntime(this.makeRuntimeFactory(), {
2656
2676
  cwd: abs,
2657
2677
  agentDir: this.agentDir,
2658
- sessionManager: SessionManager.continueRecent(abs, this.sessionDir),
2678
+ sessionManager: SessionManager.continueRecent(abs),
2659
2679
  });
2660
2680
  const conv = this.makeConversation(newRuntime);
2661
2681
  this.convs.set(conv.id, conv);
@@ -2816,7 +2836,6 @@ export class ClientSession {
2816
2836
  }
2817
2837
  export class AgentService {
2818
2838
  cwd;
2819
- sessionDirRoot;
2820
2839
  clients = new Map();
2821
2840
  pending = new Map();
2822
2841
  stateStore;
@@ -2825,9 +2844,8 @@ export class AgentService {
2825
2844
  * self-update; returns whether the process will restart itself.
2826
2845
  */
2827
2846
  onUpdateReady = undefined;
2828
- constructor(cwd, sessionDirRoot, stateFile) {
2847
+ constructor(cwd, stateFile) {
2829
2848
  this.cwd = cwd;
2830
- this.sessionDirRoot = sessionDirRoot;
2831
2849
  this.stateStore = new ClientStateStore(stateFile);
2832
2850
  }
2833
2851
  /** Get or create the session for a client, racing attach calls safely. */
@@ -2852,7 +2870,8 @@ export class AgentService {
2852
2870
  // gone (unmounted drive / deleted) — fall back to the default
2853
2871
  }
2854
2872
  }
2855
- const creating = ClientSession.create(clientId, cwd, join(this.sessionDirRoot, sanitizeId(clientId)), this.stateStore).finally(() => {
2873
+ // Sessions use the SDK default per-project dir no per-client dir.
2874
+ const creating = ClientSession.create(clientId, cwd, this.stateStore).finally(() => {
2856
2875
  this.pending.delete(clientId);
2857
2876
  });
2858
2877
  this.pending.set(clientId, creating);