pi-web-ui 0.9.1 → 0.9.3
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 +127 -50
- package/package.json +2 -1
- package/web/dist/assets/index-BZvXBQFP.css +41 -0
- package/web/dist/assets/{index-DTwK1ZcB.js → index-CFs6gldr.js} +39 -39
- 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) {
|
|
@@ -965,6 +972,9 @@ export class ClientSession {
|
|
|
965
972
|
? { id: model.id, name: model.name, provider: model.provider }
|
|
966
973
|
: null,
|
|
967
974
|
thinkingLevel: state.thinkingLevel,
|
|
975
|
+
// Only the levels the current model actually supports — the SDK clamps
|
|
976
|
+
// anything else, so the UI must not offer (or must disable) the rest.
|
|
977
|
+
availableThinkingLevels: this.session.getAvailableThinkingLevels(),
|
|
968
978
|
queue: { steering: conv.queueSteering, followUp: conv.queueFollowUp },
|
|
969
979
|
errorMessage: state.errorMessage,
|
|
970
980
|
tools: state.tools.map((t) => t.name),
|
|
@@ -1574,6 +1584,11 @@ export class ClientSession {
|
|
|
1574
1584
|
conv.title = trimmed.length > 30 ? `${trimmed.slice(0, 30)}…` : trimmed;
|
|
1575
1585
|
this.emitConversations();
|
|
1576
1586
|
}
|
|
1587
|
+
// The active conversation has been continued since it was opened — it
|
|
1588
|
+
// must not be dismissed when the user switches away. (Also bumps the
|
|
1589
|
+
// per-project "most recently active" order used by set_cwd.)
|
|
1590
|
+
conv.promptedSinceActive = true;
|
|
1591
|
+
conv.lastActiveAt = Date.now();
|
|
1577
1592
|
this.flushSnapshot();
|
|
1578
1593
|
}
|
|
1579
1594
|
/**
|
|
@@ -1865,7 +1880,9 @@ export class ClientSession {
|
|
|
1865
1880
|
async newChat() {
|
|
1866
1881
|
// Reuse an already-open blank conversation instead of piling up new ones
|
|
1867
1882
|
// 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
|
|
1883
|
+
// (focus already on it); otherwise switch to the first blank one (under
|
|
1884
|
+
// the per-project running-list model displaced blanks are disposed, so
|
|
1885
|
+
// this branch normally can't exist — kept as a safety net).
|
|
1869
1886
|
const isBlank = (c) => {
|
|
1870
1887
|
try {
|
|
1871
1888
|
return c.session.getSessionStats().totalMessages === 0;
|
|
@@ -1889,14 +1906,21 @@ export class ClientSession {
|
|
|
1889
1906
|
return;
|
|
1890
1907
|
}
|
|
1891
1908
|
}
|
|
1892
|
-
|
|
1909
|
+
// Cap is per project — conversations of other projects keep their own
|
|
1910
|
+
// lists and don't consume this project's slots.
|
|
1911
|
+
const openInProject = [...this.convs.values()].filter((c) => c.cwd === this.cwd).length;
|
|
1912
|
+
if (openInProject >= MAX_OPEN_CONVERSATIONS) {
|
|
1893
1913
|
this.emit({
|
|
1894
1914
|
type: "notice",
|
|
1895
1915
|
level: "warning",
|
|
1896
|
-
text:
|
|
1916
|
+
text: `当前项目运行的对话已达上限(${MAX_OPEN_CONVERSATIONS} 个),请先打开某个对话并离开(不继续对话)以移出列表`,
|
|
1897
1917
|
});
|
|
1898
1918
|
return;
|
|
1899
1919
|
}
|
|
1920
|
+
// The outgoing conversation is left behind — apply the running-list
|
|
1921
|
+
// lifecycle. Removal is deferred until the new chat exists so the active
|
|
1922
|
+
// conversation stays valid during the (async) runtime creation.
|
|
1923
|
+
const displaced = this.displaceActive();
|
|
1900
1924
|
try {
|
|
1901
1925
|
const runtime = await createAgentSessionRuntime(this.makeRuntimeFactory(), {
|
|
1902
1926
|
cwd: this.cwd,
|
|
@@ -1906,6 +1930,8 @@ export class ClientSession {
|
|
|
1906
1930
|
const conv = this.makeConversation(runtime);
|
|
1907
1931
|
this.convs.set(conv.id, conv);
|
|
1908
1932
|
this.activeId = conv.id;
|
|
1933
|
+
if (displaced)
|
|
1934
|
+
this.removeConversation(displaced.id);
|
|
1909
1935
|
await this.bindSession();
|
|
1910
1936
|
this.emitConversations();
|
|
1911
1937
|
}
|
|
@@ -1918,35 +1944,63 @@ export class ClientSession {
|
|
|
1918
1944
|
}
|
|
1919
1945
|
this.flushSnapshot();
|
|
1920
1946
|
}
|
|
1947
|
+
/**
|
|
1948
|
+
* The active conversation is being left (new_chat / switch_conversation /
|
|
1949
|
+
* set_cwd). Runs the running-list lifecycle:
|
|
1950
|
+
*
|
|
1951
|
+
* - still streaming → it becomes a background run: ensure it is listed;
|
|
1952
|
+
* - idle + listed + continued → keep it (the user did continue it);
|
|
1953
|
+
* - idle + listed + opened-but-not-continued, or never listed at all → the
|
|
1954
|
+
* caller must drop it (returns it so removal happens only after the
|
|
1955
|
+
* active conversation has been switched away).
|
|
1956
|
+
*/
|
|
1957
|
+
displaceActive() {
|
|
1958
|
+
const conv = this.conv;
|
|
1959
|
+
if (conv.session.isStreaming) {
|
|
1960
|
+
conv.listed = true;
|
|
1961
|
+
return null;
|
|
1962
|
+
}
|
|
1963
|
+
if (conv.listed && conv.promptedSinceActive)
|
|
1964
|
+
return null;
|
|
1965
|
+
return conv;
|
|
1966
|
+
}
|
|
1967
|
+
/** Remove a conversation from the running list and free its runtime. The
|
|
1968
|
+
* session stays persisted on disk, so it remains recoverable from the
|
|
1969
|
+
* history list. Never removes the active conversation. */
|
|
1970
|
+
removeConversation(id) {
|
|
1971
|
+
const conv = this.convs.get(id);
|
|
1972
|
+
if (!conv || id === this.activeId)
|
|
1973
|
+
return;
|
|
1974
|
+
this.convs.delete(id);
|
|
1975
|
+
conv.unsubscribe?.();
|
|
1976
|
+
conv.unsubscribe = undefined;
|
|
1977
|
+
void conv.runtime.dispose().catch(() => { });
|
|
1978
|
+
}
|
|
1921
1979
|
/** Switch the ACTIVE conversation without interrupting any other chat. */
|
|
1922
1980
|
async switchConversation(id) {
|
|
1923
1981
|
if (!this.convs.has(id) || id === this.activeId)
|
|
1924
1982
|
return;
|
|
1925
|
-
const
|
|
1983
|
+
const displaced = this.displaceActive();
|
|
1926
1984
|
this.activeId = id;
|
|
1927
1985
|
this.cwd = this.conv.cwd;
|
|
1928
|
-
//
|
|
1929
|
-
//
|
|
1930
|
-
if (
|
|
1931
|
-
this.
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
text: `已切换到工作目录:${this.cwd}`,
|
|
1935
|
-
});
|
|
1936
|
-
}
|
|
1986
|
+
// All listed conversations share the current project's cwd, so this is
|
|
1987
|
+
// normally a no-op — kept defensive for stale clients.
|
|
1988
|
+
if (displaced)
|
|
1989
|
+
this.removeConversation(displaced.id);
|
|
1990
|
+
this.conv.promptedSinceActive = false;
|
|
1991
|
+
this.conv.lastActiveAt = Date.now();
|
|
1937
1992
|
this.webUi.refresh();
|
|
1938
1993
|
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
1994
|
this.flushSnapshot();
|
|
1945
1995
|
}
|
|
1946
|
-
/** Push the
|
|
1996
|
+
/** Push the current project's running-conversation list to the client. */
|
|
1947
1997
|
emitConversations() {
|
|
1948
1998
|
const conversations = [];
|
|
1949
1999
|
for (const conv of this.convs.values()) {
|
|
2000
|
+
// The running-conversation list is per project and only contains
|
|
2001
|
+
// conversations that were displaced to the background while running.
|
|
2002
|
+
if (conv.cwd !== this.cwd || !conv.listed)
|
|
2003
|
+
continue;
|
|
1950
2004
|
let messageCount = 0;
|
|
1951
2005
|
let isStreaming = false;
|
|
1952
2006
|
try {
|
|
@@ -2030,6 +2084,9 @@ export class ClientSession {
|
|
|
2030
2084
|
this.conv.cwd = this.runtime.cwd;
|
|
2031
2085
|
this.cwd = this.runtime.cwd;
|
|
2032
2086
|
this.conv.title = conversationTitle(this.runtime.session);
|
|
2087
|
+
// Deliberately resumed — must not be dismissed when the user later
|
|
2088
|
+
// switches away without sending a new message.
|
|
2089
|
+
this.conv.promptedSinceActive = true;
|
|
2033
2090
|
this.emitConversations();
|
|
2034
2091
|
}
|
|
2035
2092
|
catch (err) {
|
|
@@ -2371,9 +2428,12 @@ export class ClientSession {
|
|
|
2371
2428
|
}
|
|
2372
2429
|
}
|
|
2373
2430
|
/**
|
|
2374
|
-
* Switch the agent's working directory by
|
|
2375
|
-
*
|
|
2376
|
-
*
|
|
2431
|
+
* Switch the agent's working directory by switching the ACTIVE conversation
|
|
2432
|
+
* to the target project's own most recently active conversation (creating a
|
|
2433
|
+
* fresh one that resumes that project's most recent session on first
|
|
2434
|
+
* visit). Conversations of other projects keep running untouched in their
|
|
2435
|
+
* own per-project lists — nothing is rebuilt, so titles/cwds never leak
|
|
2436
|
+
* between projects.
|
|
2377
2437
|
*/
|
|
2378
2438
|
async setCwd(newCwd) {
|
|
2379
2439
|
try {
|
|
@@ -2393,40 +2453,57 @@ export class ClientSession {
|
|
|
2393
2453
|
this.flushSnapshot();
|
|
2394
2454
|
return;
|
|
2395
2455
|
}
|
|
2396
|
-
//
|
|
2397
|
-
//
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
const
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2456
|
+
// The outgoing conversation is left behind — apply the running-list
|
|
2457
|
+
// lifecycle (removal is deferred until the active conversation is
|
|
2458
|
+
// safely switched away).
|
|
2459
|
+
const displaced = this.displaceActive();
|
|
2460
|
+
// Prefer the target project's own most recently active conversation;
|
|
2461
|
+
// only create a fresh one (resuming its most recent session) when the
|
|
2462
|
+
// project has none open yet.
|
|
2463
|
+
let target;
|
|
2464
|
+
for (const c of this.convs.values()) {
|
|
2465
|
+
if (c.cwd === abs &&
|
|
2466
|
+
(!target || c.lastActiveAt > target.lastActiveAt)) {
|
|
2467
|
+
target = c;
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
if (target) {
|
|
2471
|
+
this.activeId = target.id;
|
|
2472
|
+
if (displaced)
|
|
2473
|
+
this.removeConversation(displaced.id);
|
|
2474
|
+
}
|
|
2475
|
+
else {
|
|
2476
|
+
// First visit to this project: resume its most recent session.
|
|
2477
|
+
const newRuntime = await createAgentSessionRuntime(this.makeRuntimeFactory(), {
|
|
2478
|
+
cwd: abs,
|
|
2479
|
+
agentDir: this.agentDir,
|
|
2480
|
+
sessionManager: SessionManager.continueRecent(abs, this.sessionDir),
|
|
2481
|
+
});
|
|
2482
|
+
const conv = this.makeConversation(newRuntime);
|
|
2483
|
+
this.convs.set(conv.id, conv);
|
|
2484
|
+
this.activeId = conv.id;
|
|
2485
|
+
if (displaced)
|
|
2486
|
+
this.removeConversation(displaced.id);
|
|
2487
|
+
for (const d of newRuntime.diagnostics) {
|
|
2488
|
+
if (d.type !== "info") {
|
|
2489
|
+
this.emit({ type: "notice", level: d.type, text: d.message });
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
await this.bindSession();
|
|
2493
|
+
}
|
|
2494
|
+
this.conv.promptedSinceActive = false;
|
|
2495
|
+
this.conv.lastActiveAt = Date.now();
|
|
2411
2496
|
this.cwd = abs;
|
|
2412
2497
|
// Remember the new workspace (restore target + recent-project entry).
|
|
2413
2498
|
this.stateStore.remember(this.clientId, abs);
|
|
2414
2499
|
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
|
-
}
|
|
2500
|
+
this.webUi.refresh();
|
|
2501
|
+
this.emitConversations();
|
|
2424
2502
|
this.emit({
|
|
2425
2503
|
type: "notice",
|
|
2426
2504
|
level: "info",
|
|
2427
2505
|
text: `已切换到工作目录:${abs}`,
|
|
2428
2506
|
});
|
|
2429
|
-
this.emitConversations();
|
|
2430
2507
|
void this.refreshSessions();
|
|
2431
2508
|
void this.listFiles(undefined);
|
|
2432
2509
|
// 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.3",
|
|
4
|
+
"version": "0.9.3",
|
|
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",
|