pi-web-ui 0.9.0 → 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 +149 -49
- 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
|
/**
|
|
@@ -1863,14 +1875,49 @@ export class ClientSession {
|
|
|
1863
1875
|
this.flushSnapshot();
|
|
1864
1876
|
}
|
|
1865
1877
|
async newChat() {
|
|
1866
|
-
|
|
1878
|
+
// Reuse an already-open blank conversation instead of piling up new ones
|
|
1879
|
+
// on every click: if the active chat has no messages it IS the new chat
|
|
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).
|
|
1883
|
+
const isBlank = (c) => {
|
|
1884
|
+
try {
|
|
1885
|
+
return c.session.getSessionStats().totalMessages === 0;
|
|
1886
|
+
}
|
|
1887
|
+
catch {
|
|
1888
|
+
// session being replaced — treat as used so we don't switch onto it
|
|
1889
|
+
return false;
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
const active = this.conv;
|
|
1893
|
+
if (active && isBlank(active)) {
|
|
1894
|
+
this.flushSnapshot();
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
for (const conv of this.convs.values()) {
|
|
1898
|
+
if (conv.id === this.activeId)
|
|
1899
|
+
continue;
|
|
1900
|
+
if (isBlank(conv)) {
|
|
1901
|
+
await this.switchConversation(conv.id);
|
|
1902
|
+
this.flushSnapshot();
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
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) {
|
|
1867
1910
|
this.emit({
|
|
1868
1911
|
type: "notice",
|
|
1869
1912
|
level: "warning",
|
|
1870
|
-
text:
|
|
1913
|
+
text: `当前项目运行的对话已达上限(${MAX_OPEN_CONVERSATIONS} 个),请先打开某个对话并离开(不继续对话)以移出列表`,
|
|
1871
1914
|
});
|
|
1872
1915
|
return;
|
|
1873
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();
|
|
1874
1921
|
try {
|
|
1875
1922
|
const runtime = await createAgentSessionRuntime(this.makeRuntimeFactory(), {
|
|
1876
1923
|
cwd: this.cwd,
|
|
@@ -1880,6 +1927,8 @@ export class ClientSession {
|
|
|
1880
1927
|
const conv = this.makeConversation(runtime);
|
|
1881
1928
|
this.convs.set(conv.id, conv);
|
|
1882
1929
|
this.activeId = conv.id;
|
|
1930
|
+
if (displaced)
|
|
1931
|
+
this.removeConversation(displaced.id);
|
|
1883
1932
|
await this.bindSession();
|
|
1884
1933
|
this.emitConversations();
|
|
1885
1934
|
}
|
|
@@ -1892,35 +1941,63 @@ export class ClientSession {
|
|
|
1892
1941
|
}
|
|
1893
1942
|
this.flushSnapshot();
|
|
1894
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
|
+
}
|
|
1895
1976
|
/** Switch the ACTIVE conversation without interrupting any other chat. */
|
|
1896
1977
|
async switchConversation(id) {
|
|
1897
1978
|
if (!this.convs.has(id) || id === this.activeId)
|
|
1898
1979
|
return;
|
|
1899
|
-
const
|
|
1980
|
+
const displaced = this.displaceActive();
|
|
1900
1981
|
this.activeId = id;
|
|
1901
1982
|
this.cwd = this.conv.cwd;
|
|
1902
|
-
//
|
|
1903
|
-
//
|
|
1904
|
-
if (
|
|
1905
|
-
this.
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
text: `已切换到工作目录:${this.cwd}`,
|
|
1909
|
-
});
|
|
1910
|
-
}
|
|
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();
|
|
1911
1989
|
this.webUi.refresh();
|
|
1912
1990
|
this.emitConversations();
|
|
1913
|
-
// Workspace-bound panels (session list / file tree / commands) follow
|
|
1914
|
-
// the active conversation's cwd.
|
|
1915
|
-
void this.refreshSessions();
|
|
1916
|
-
void this.listFiles(undefined);
|
|
1917
|
-
void this.listCommands();
|
|
1918
1991
|
this.flushSnapshot();
|
|
1919
1992
|
}
|
|
1920
|
-
/** Push the
|
|
1993
|
+
/** Push the current project's running-conversation list to the client. */
|
|
1921
1994
|
emitConversations() {
|
|
1922
1995
|
const conversations = [];
|
|
1923
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;
|
|
1924
2001
|
let messageCount = 0;
|
|
1925
2002
|
let isStreaming = false;
|
|
1926
2003
|
try {
|
|
@@ -2004,6 +2081,9 @@ export class ClientSession {
|
|
|
2004
2081
|
this.conv.cwd = this.runtime.cwd;
|
|
2005
2082
|
this.cwd = this.runtime.cwd;
|
|
2006
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;
|
|
2007
2087
|
this.emitConversations();
|
|
2008
2088
|
}
|
|
2009
2089
|
catch (err) {
|
|
@@ -2345,9 +2425,12 @@ export class ClientSession {
|
|
|
2345
2425
|
}
|
|
2346
2426
|
}
|
|
2347
2427
|
/**
|
|
2348
|
-
* Switch the agent's working directory by
|
|
2349
|
-
*
|
|
2350
|
-
*
|
|
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.
|
|
2351
2434
|
*/
|
|
2352
2435
|
async setCwd(newCwd) {
|
|
2353
2436
|
try {
|
|
@@ -2367,40 +2450,57 @@ export class ClientSession {
|
|
|
2367
2450
|
this.flushSnapshot();
|
|
2368
2451
|
return;
|
|
2369
2452
|
}
|
|
2370
|
-
//
|
|
2371
|
-
//
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
const
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
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();
|
|
2385
2493
|
this.cwd = abs;
|
|
2386
2494
|
// Remember the new workspace (restore target + recent-project entry).
|
|
2387
2495
|
this.stateStore.remember(this.clientId, abs);
|
|
2388
2496
|
void this.pushProjects();
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
await this.bindSession();
|
|
2392
|
-
await oldRuntime.dispose().catch(() => { });
|
|
2393
|
-
for (const d of newRuntime.diagnostics) {
|
|
2394
|
-
if (d.type !== "info") {
|
|
2395
|
-
this.emit({ type: "notice", level: d.type, text: d.message });
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2497
|
+
this.webUi.refresh();
|
|
2498
|
+
this.emitConversations();
|
|
2398
2499
|
this.emit({
|
|
2399
2500
|
type: "notice",
|
|
2400
2501
|
level: "info",
|
|
2401
2502
|
text: `已切换到工作目录:${abs}`,
|
|
2402
2503
|
});
|
|
2403
|
-
this.emitConversations();
|
|
2404
2504
|
void this.refreshSessions();
|
|
2405
2505
|
void this.listFiles(undefined);
|
|
2406
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",
|