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/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
@@ -617,7 +617,8 @@ class ClientStateStore {
617
617
  this.save();
618
618
  }
619
619
  }
620
- /** Cap on simultaneously open conversations (each keeps a full runtime alive). */
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 open-conversation list so the left panel shows
796
- // every chat (a fresh socket never got the newChat/switch pushes).
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
- if (this.convs.size >= MAX_OPEN_CONVERSATIONS) {
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: `打开的对话已达上限(${MAX_OPEN_CONVERSATIONS} 个),请先切换或重载页面关闭`,
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 prevCwd = this.cwd;
1980
+ const displaced = this.displaceActive();
1900
1981
  this.activeId = id;
1901
1982
  this.cwd = this.conv.cwd;
1902
- // Cross-directory jump: make the workspace switch explicit so the user
1903
- // isn't surprised when the file tree / footer / history all change.
1904
- if (this.cwd !== prevCwd) {
1905
- this.emit({
1906
- type: "notice",
1907
- level: "info",
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 open-conversation list to the client. */
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 rebuilding the runtime for the new
2349
- * cwd (services are cwd-bound). Resumes that directory's most recent session;
2350
- * refreshes snapshot, session list, and file tree.
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
- // Build the new runtime firstonly swap the ACTIVE conversation on
2371
- // success (other open conversations keep their own cwd + runtime).
2372
- const newRuntime = await createAgentSessionRuntime(this.makeRuntimeFactory(), {
2373
- cwd: abs,
2374
- agentDir: this.agentDir,
2375
- sessionManager: SessionManager.continueRecent(abs, this.sessionDir),
2376
- });
2377
- const conv = this.conv;
2378
- const oldRuntime = conv.runtime;
2379
- conv.runtime = newRuntime;
2380
- conv.session = newRuntime.session;
2381
- conv.cwd = abs;
2382
- // The rebuilt runtime resumed the new project's most recent session —
2383
- // refresh the title too, or the conversation list shows the old name.
2384
- conv.title = conversationTitle(newRuntime.session);
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
- conv.unsubscribe?.();
2390
- conv.unsubscribe = undefined;
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.0",
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",