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/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
  /**
@@ -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
- if (this.convs.size >= MAX_OPEN_CONVERSATIONS) {
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: `打开的对话已达上限(${MAX_OPEN_CONVERSATIONS} 个),请先切换或重载页面关闭`,
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 prevCwd = this.cwd;
1980
+ const displaced = this.displaceActive();
1926
1981
  this.activeId = id;
1927
1982
  this.cwd = this.conv.cwd;
1928
- // Cross-directory jump: make the workspace switch explicit so the user
1929
- // isn't surprised when the file tree / footer / history all change.
1930
- if (this.cwd !== prevCwd) {
1931
- this.emit({
1932
- type: "notice",
1933
- level: "info",
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 open-conversation list to the client. */
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 rebuilding the runtime for the new
2375
- * cwd (services are cwd-bound). Resumes that directory's most recent session;
2376
- * 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.
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
- // Build the new runtime firstonly swap the ACTIVE conversation on
2397
- // success (other open conversations keep their own cwd + runtime).
2398
- const newRuntime = await createAgentSessionRuntime(this.makeRuntimeFactory(), {
2399
- cwd: abs,
2400
- agentDir: this.agentDir,
2401
- sessionManager: SessionManager.continueRecent(abs, this.sessionDir),
2402
- });
2403
- const conv = this.conv;
2404
- const oldRuntime = conv.runtime;
2405
- conv.runtime = newRuntime;
2406
- conv.session = newRuntime.session;
2407
- conv.cwd = abs;
2408
- // The rebuilt runtime resumed the new project's most recent session —
2409
- // refresh the title too, or the conversation list shows the old name.
2410
- 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();
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
- conv.unsubscribe?.();
2416
- conv.unsubscribe = undefined;
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.1",
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",