pi-web-ui 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +397 -414
- package/README.zh-CN.md +359 -359
- package/bin/pi-web-ui.mjs +0 -0
- package/deploy/com.xingshuyin.pi-web-ui.plist +48 -48
- package/deploy/pi-web-ui-task.xml +71 -71
- package/deploy/pi-web-ui.service +31 -31
- package/dist/server/agent-service.js +124 -50
- package/package.json +2 -1
- package/web/dist/assets/{index-DTwK1ZcB.js → index-AuwK31s1.js} +25 -25
- package/web/dist/assets/index-cb8yWnzI.css +41 -0
- package/web/dist/favicon.svg +8 -8
- package/web/dist/index.html +15 -15
- package/web/dist/assets/index-8BJsbrQQ.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
|
|
@@ -617,7 +617,8 @@ class ClientStateStore {
|
|
|
617
617
|
this.save();
|
|
618
618
|
}
|
|
619
619
|
}
|
|
620
|
-
/** Cap on simultaneously open conversations (each keeps a full
|
|
620
|
+
/** Cap on simultaneously open conversations of ONE project (each keeps a full
|
|
621
|
+
* runtime alive; conversations of other projects keep their own lists). */
|
|
621
622
|
const MAX_OPEN_CONVERSATIONS = 8;
|
|
622
623
|
const DEFAULT_CONV_TITLE = "新对话";
|
|
623
624
|
/** First user text in a session, truncated for the conversation list. */
|
|
@@ -769,6 +770,11 @@ export class ClientSession {
|
|
|
769
770
|
session: runtime.session,
|
|
770
771
|
cwd: runtime.cwd,
|
|
771
772
|
createdAt: Date.now(),
|
|
773
|
+
// A brand-new conversation is not yet in the running list — it enters
|
|
774
|
+
// only when it is displaced to the background while still streaming.
|
|
775
|
+
listed: false,
|
|
776
|
+
promptedSinceActive: false,
|
|
777
|
+
lastActiveAt: Date.now(),
|
|
772
778
|
msgIds: new Map(),
|
|
773
779
|
nextMsgId: 1,
|
|
774
780
|
uiMessageCache: new Map(),
|
|
@@ -792,8 +798,9 @@ export class ClientSession {
|
|
|
792
798
|
const statuses = this.webUi.statusSnapshot();
|
|
793
799
|
if (statuses.length > 0)
|
|
794
800
|
send({ type: "statuses", statuses });
|
|
795
|
-
// Reconnect: push the
|
|
796
|
-
// every chat (a fresh socket never got the
|
|
801
|
+
// Reconnect: push the current project's running-conversation list so the
|
|
802
|
+
// left panel shows every background chat (a fresh socket never got the
|
|
803
|
+
// newChat/switch pushes).
|
|
797
804
|
this.emitConversations();
|
|
798
805
|
}
|
|
799
806
|
detachSink(send) {
|
|
@@ -1574,6 +1581,11 @@ export class ClientSession {
|
|
|
1574
1581
|
conv.title = trimmed.length > 30 ? `${trimmed.slice(0, 30)}…` : trimmed;
|
|
1575
1582
|
this.emitConversations();
|
|
1576
1583
|
}
|
|
1584
|
+
// The active conversation has been continued since it was opened — it
|
|
1585
|
+
// must not be dismissed when the user switches away. (Also bumps the
|
|
1586
|
+
// per-project "most recently active" order used by set_cwd.)
|
|
1587
|
+
conv.promptedSinceActive = true;
|
|
1588
|
+
conv.lastActiveAt = Date.now();
|
|
1577
1589
|
this.flushSnapshot();
|
|
1578
1590
|
}
|
|
1579
1591
|
/**
|
|
@@ -1865,7 +1877,9 @@ export class ClientSession {
|
|
|
1865
1877
|
async newChat() {
|
|
1866
1878
|
// Reuse an already-open blank conversation instead of piling up new ones
|
|
1867
1879
|
// 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
|
|
1880
|
+
// (focus already on it); otherwise switch to the first blank one (under
|
|
1881
|
+
// the per-project running-list model displaced blanks are disposed, so
|
|
1882
|
+
// this branch normally can't exist — kept as a safety net).
|
|
1869
1883
|
const isBlank = (c) => {
|
|
1870
1884
|
try {
|
|
1871
1885
|
return c.session.getSessionStats().totalMessages === 0;
|
|
@@ -1889,14 +1903,21 @@ export class ClientSession {
|
|
|
1889
1903
|
return;
|
|
1890
1904
|
}
|
|
1891
1905
|
}
|
|
1892
|
-
|
|
1906
|
+
// Cap is per project — conversations of other projects keep their own
|
|
1907
|
+
// lists and don't consume this project's slots.
|
|
1908
|
+
const openInProject = [...this.convs.values()].filter((c) => c.cwd === this.cwd).length;
|
|
1909
|
+
if (openInProject >= MAX_OPEN_CONVERSATIONS) {
|
|
1893
1910
|
this.emit({
|
|
1894
1911
|
type: "notice",
|
|
1895
1912
|
level: "warning",
|
|
1896
|
-
text:
|
|
1913
|
+
text: `当前项目运行的对话已达上限(${MAX_OPEN_CONVERSATIONS} 个),请先打开某个对话并离开(不继续对话)以移出列表`,
|
|
1897
1914
|
});
|
|
1898
1915
|
return;
|
|
1899
1916
|
}
|
|
1917
|
+
// The outgoing conversation is left behind — apply the running-list
|
|
1918
|
+
// lifecycle. Removal is deferred until the new chat exists so the active
|
|
1919
|
+
// conversation stays valid during the (async) runtime creation.
|
|
1920
|
+
const displaced = this.displaceActive();
|
|
1900
1921
|
try {
|
|
1901
1922
|
const runtime = await createAgentSessionRuntime(this.makeRuntimeFactory(), {
|
|
1902
1923
|
cwd: this.cwd,
|
|
@@ -1906,6 +1927,8 @@ export class ClientSession {
|
|
|
1906
1927
|
const conv = this.makeConversation(runtime);
|
|
1907
1928
|
this.convs.set(conv.id, conv);
|
|
1908
1929
|
this.activeId = conv.id;
|
|
1930
|
+
if (displaced)
|
|
1931
|
+
this.removeConversation(displaced.id);
|
|
1909
1932
|
await this.bindSession();
|
|
1910
1933
|
this.emitConversations();
|
|
1911
1934
|
}
|
|
@@ -1918,35 +1941,63 @@ export class ClientSession {
|
|
|
1918
1941
|
}
|
|
1919
1942
|
this.flushSnapshot();
|
|
1920
1943
|
}
|
|
1944
|
+
/**
|
|
1945
|
+
* The active conversation is being left (new_chat / switch_conversation /
|
|
1946
|
+
* set_cwd). Runs the running-list lifecycle:
|
|
1947
|
+
*
|
|
1948
|
+
* - still streaming → it becomes a background run: ensure it is listed;
|
|
1949
|
+
* - idle + listed + continued → keep it (the user did continue it);
|
|
1950
|
+
* - idle + listed + opened-but-not-continued, or never listed at all → the
|
|
1951
|
+
* caller must drop it (returns it so removal happens only after the
|
|
1952
|
+
* active conversation has been switched away).
|
|
1953
|
+
*/
|
|
1954
|
+
displaceActive() {
|
|
1955
|
+
const conv = this.conv;
|
|
1956
|
+
if (conv.session.isStreaming) {
|
|
1957
|
+
conv.listed = true;
|
|
1958
|
+
return null;
|
|
1959
|
+
}
|
|
1960
|
+
if (conv.listed && conv.promptedSinceActive)
|
|
1961
|
+
return null;
|
|
1962
|
+
return conv;
|
|
1963
|
+
}
|
|
1964
|
+
/** Remove a conversation from the running list and free its runtime. The
|
|
1965
|
+
* session stays persisted on disk, so it remains recoverable from the
|
|
1966
|
+
* history list. Never removes the active conversation. */
|
|
1967
|
+
removeConversation(id) {
|
|
1968
|
+
const conv = this.convs.get(id);
|
|
1969
|
+
if (!conv || id === this.activeId)
|
|
1970
|
+
return;
|
|
1971
|
+
this.convs.delete(id);
|
|
1972
|
+
conv.unsubscribe?.();
|
|
1973
|
+
conv.unsubscribe = undefined;
|
|
1974
|
+
void conv.runtime.dispose().catch(() => { });
|
|
1975
|
+
}
|
|
1921
1976
|
/** Switch the ACTIVE conversation without interrupting any other chat. */
|
|
1922
1977
|
async switchConversation(id) {
|
|
1923
1978
|
if (!this.convs.has(id) || id === this.activeId)
|
|
1924
1979
|
return;
|
|
1925
|
-
const
|
|
1980
|
+
const displaced = this.displaceActive();
|
|
1926
1981
|
this.activeId = id;
|
|
1927
1982
|
this.cwd = this.conv.cwd;
|
|
1928
|
-
//
|
|
1929
|
-
//
|
|
1930
|
-
if (
|
|
1931
|
-
this.
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
text: `已切换到工作目录:${this.cwd}`,
|
|
1935
|
-
});
|
|
1936
|
-
}
|
|
1983
|
+
// All listed conversations share the current project's cwd, so this is
|
|
1984
|
+
// normally a no-op — kept defensive for stale clients.
|
|
1985
|
+
if (displaced)
|
|
1986
|
+
this.removeConversation(displaced.id);
|
|
1987
|
+
this.conv.promptedSinceActive = false;
|
|
1988
|
+
this.conv.lastActiveAt = Date.now();
|
|
1937
1989
|
this.webUi.refresh();
|
|
1938
1990
|
this.emitConversations();
|
|
1939
|
-
// Workspace-bound panels (session list / file tree / commands) follow
|
|
1940
|
-
// the active conversation's cwd.
|
|
1941
|
-
void this.refreshSessions();
|
|
1942
|
-
void this.listFiles(undefined);
|
|
1943
|
-
void this.listCommands();
|
|
1944
1991
|
this.flushSnapshot();
|
|
1945
1992
|
}
|
|
1946
|
-
/** Push the
|
|
1993
|
+
/** Push the current project's running-conversation list to the client. */
|
|
1947
1994
|
emitConversations() {
|
|
1948
1995
|
const conversations = [];
|
|
1949
1996
|
for (const conv of this.convs.values()) {
|
|
1997
|
+
// The running-conversation list is per project and only contains
|
|
1998
|
+
// conversations that were displaced to the background while running.
|
|
1999
|
+
if (conv.cwd !== this.cwd || !conv.listed)
|
|
2000
|
+
continue;
|
|
1950
2001
|
let messageCount = 0;
|
|
1951
2002
|
let isStreaming = false;
|
|
1952
2003
|
try {
|
|
@@ -2030,6 +2081,9 @@ export class ClientSession {
|
|
|
2030
2081
|
this.conv.cwd = this.runtime.cwd;
|
|
2031
2082
|
this.cwd = this.runtime.cwd;
|
|
2032
2083
|
this.conv.title = conversationTitle(this.runtime.session);
|
|
2084
|
+
// Deliberately resumed — must not be dismissed when the user later
|
|
2085
|
+
// switches away without sending a new message.
|
|
2086
|
+
this.conv.promptedSinceActive = true;
|
|
2033
2087
|
this.emitConversations();
|
|
2034
2088
|
}
|
|
2035
2089
|
catch (err) {
|
|
@@ -2371,9 +2425,12 @@ export class ClientSession {
|
|
|
2371
2425
|
}
|
|
2372
2426
|
}
|
|
2373
2427
|
/**
|
|
2374
|
-
* Switch the agent's working directory by
|
|
2375
|
-
*
|
|
2376
|
-
*
|
|
2428
|
+
* Switch the agent's working directory by switching the ACTIVE conversation
|
|
2429
|
+
* to the target project's own most recently active conversation (creating a
|
|
2430
|
+
* fresh one that resumes that project's most recent session on first
|
|
2431
|
+
* visit). Conversations of other projects keep running untouched in their
|
|
2432
|
+
* own per-project lists — nothing is rebuilt, so titles/cwds never leak
|
|
2433
|
+
* between projects.
|
|
2377
2434
|
*/
|
|
2378
2435
|
async setCwd(newCwd) {
|
|
2379
2436
|
try {
|
|
@@ -2393,40 +2450,57 @@ export class ClientSession {
|
|
|
2393
2450
|
this.flushSnapshot();
|
|
2394
2451
|
return;
|
|
2395
2452
|
}
|
|
2396
|
-
//
|
|
2397
|
-
//
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
const
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2453
|
+
// The outgoing conversation is left behind — apply the running-list
|
|
2454
|
+
// lifecycle (removal is deferred until the active conversation is
|
|
2455
|
+
// safely switched away).
|
|
2456
|
+
const displaced = this.displaceActive();
|
|
2457
|
+
// Prefer the target project's own most recently active conversation;
|
|
2458
|
+
// only create a fresh one (resuming its most recent session) when the
|
|
2459
|
+
// project has none open yet.
|
|
2460
|
+
let target;
|
|
2461
|
+
for (const c of this.convs.values()) {
|
|
2462
|
+
if (c.cwd === abs &&
|
|
2463
|
+
(!target || c.lastActiveAt > target.lastActiveAt)) {
|
|
2464
|
+
target = c;
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
if (target) {
|
|
2468
|
+
this.activeId = target.id;
|
|
2469
|
+
if (displaced)
|
|
2470
|
+
this.removeConversation(displaced.id);
|
|
2471
|
+
}
|
|
2472
|
+
else {
|
|
2473
|
+
// First visit to this project: resume its most recent session.
|
|
2474
|
+
const newRuntime = await createAgentSessionRuntime(this.makeRuntimeFactory(), {
|
|
2475
|
+
cwd: abs,
|
|
2476
|
+
agentDir: this.agentDir,
|
|
2477
|
+
sessionManager: SessionManager.continueRecent(abs, this.sessionDir),
|
|
2478
|
+
});
|
|
2479
|
+
const conv = this.makeConversation(newRuntime);
|
|
2480
|
+
this.convs.set(conv.id, conv);
|
|
2481
|
+
this.activeId = conv.id;
|
|
2482
|
+
if (displaced)
|
|
2483
|
+
this.removeConversation(displaced.id);
|
|
2484
|
+
for (const d of newRuntime.diagnostics) {
|
|
2485
|
+
if (d.type !== "info") {
|
|
2486
|
+
this.emit({ type: "notice", level: d.type, text: d.message });
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
await this.bindSession();
|
|
2490
|
+
}
|
|
2491
|
+
this.conv.promptedSinceActive = false;
|
|
2492
|
+
this.conv.lastActiveAt = Date.now();
|
|
2411
2493
|
this.cwd = abs;
|
|
2412
2494
|
// Remember the new workspace (restore target + recent-project entry).
|
|
2413
2495
|
this.stateStore.remember(this.clientId, abs);
|
|
2414
2496
|
void this.pushProjects();
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
await this.bindSession();
|
|
2418
|
-
await oldRuntime.dispose().catch(() => { });
|
|
2419
|
-
for (const d of newRuntime.diagnostics) {
|
|
2420
|
-
if (d.type !== "info") {
|
|
2421
|
-
this.emit({ type: "notice", level: d.type, text: d.message });
|
|
2422
|
-
}
|
|
2423
|
-
}
|
|
2497
|
+
this.webUi.refresh();
|
|
2498
|
+
this.emitConversations();
|
|
2424
2499
|
this.emit({
|
|
2425
2500
|
type: "notice",
|
|
2426
2501
|
level: "info",
|
|
2427
2502
|
text: `已切换到工作目录:${abs}`,
|
|
2428
2503
|
});
|
|
2429
|
-
this.emitConversations();
|
|
2430
2504
|
void this.refreshSessions();
|
|
2431
2505
|
void this.listFiles(undefined);
|
|
2432
2506
|
// Commands are per-project (.pi/commands.json in the current cwd).
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-ui",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
|
+
"version": "0.9.2",
|
|
4
5
|
"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
6
|
"license": "MIT",
|
|
6
7
|
"type": "module",
|