stashes 0.1.28 → 0.1.30

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/dist/cli.js CHANGED
@@ -247,6 +247,10 @@ class WorktreeManager {
247
247
  const { readdirSync } = await import("fs");
248
248
  const entries = readdirSync(worktreesDir);
249
249
  for (const entry of entries) {
250
+ if (entry.startsWith("screenshot-")) {
251
+ logger.info("worktree", `skipping active screenshot worktree: ${entry}`);
252
+ continue;
253
+ }
250
254
  const worktreePath = join3(worktreesDir, entry);
251
255
  logger.info("worktree", `cleaning up stale worktree: ${entry}`);
252
256
  try {
@@ -896,6 +900,20 @@ function parseAiResult(text) {
896
900
  return null;
897
901
  }
898
902
  }
903
+ async function fallbackScreenshot(port, projectPath, stashId) {
904
+ try {
905
+ const url = await captureScreenshot(port, projectPath, stashId);
906
+ return {
907
+ primary: url,
908
+ screenshots: [{ url, label: "Homepage", route: "/", isPrimary: true }]
909
+ };
910
+ } catch (err) {
911
+ logger.error("smart-screenshot", `Fallback screenshot also failed for ${stashId}`, {
912
+ error: err instanceof Error ? err.message : String(err)
913
+ });
914
+ return { primary: "", screenshots: [] };
915
+ }
916
+ }
899
917
  async function captureSmartScreenshots(opts) {
900
918
  const { projectPath, stashId, stashBranch, parentBranch, worktreePath, port, model = "haiku", timeout = DEFAULT_TIMEOUT } = opts;
901
919
  const screenshotDir = join6(projectPath, SCREENSHOTS_DIR2);
@@ -905,11 +923,7 @@ async function captureSmartScreenshots(opts) {
905
923
  const diff = await getStashDiff(worktreePath, parentBranch);
906
924
  if (!diff) {
907
925
  logger.info("smart-screenshot", `No diff found for ${stashId}, using simple screenshot`);
908
- const url = await captureScreenshot(port, projectPath, stashId);
909
- return {
910
- primary: url,
911
- screenshots: [{ url, label: "Homepage", route: "/", isPrimary: true }]
912
- };
926
+ return fallbackScreenshot(port, projectPath, stashId);
913
927
  }
914
928
  const processId = `screenshot-ai-${stashId}`;
915
929
  const prompt = buildScreenshotPrompt(port, diff, screenshotDir, stashId);
@@ -939,12 +953,8 @@ async function captureSmartScreenshots(opts) {
939
953
  }
940
954
  const result = parseAiResult(textOutput);
941
955
  if (!result || !result.screenshots || result.screenshots.length === 0) {
942
- logger.info("smart-screenshot", `AI returned no screenshots for ${stashId}, falling back`);
943
- const url = await captureScreenshot(port, projectPath, stashId);
944
- return {
945
- primary: url,
946
- screenshots: [{ url, label: "Homepage", route: "/", isPrimary: true }]
947
- };
956
+ logger.info("smart-screenshot", `AI returned no screenshots for ${stashId} (timedOut=${timedOut}), falling back`);
957
+ return fallbackScreenshot(port, projectPath, stashId);
948
958
  }
949
959
  const screenshots = [];
950
960
  let primaryUrl = "";
@@ -967,11 +977,7 @@ async function captureSmartScreenshots(opts) {
967
977
  }
968
978
  if (screenshots.length === 0) {
969
979
  logger.info("smart-screenshot", `No valid screenshots for ${stashId}, falling back`);
970
- const url = await captureScreenshot(port, projectPath, stashId);
971
- return {
972
- primary: url,
973
- screenshots: [{ url, label: "Homepage", route: "/", isPrimary: true }]
974
- };
980
+ return fallbackScreenshot(port, projectPath, stashId);
975
981
  }
976
982
  if (!primaryUrl) {
977
983
  primaryUrl = screenshots[0].url;
@@ -1120,7 +1126,7 @@ async function generate(opts) {
1120
1126
  const port = await allocatePort();
1121
1127
  const worktree = await worktreeManager.createForGeneration(`screenshot-${stash.id}`);
1122
1128
  const screenshotGit = simpleGit3(worktree.path);
1123
- await screenshotGit.checkout(["-f", stash.branch]);
1129
+ await screenshotGit.checkout(["--detach", stash.branch]);
1124
1130
  const devServer = spawn3({
1125
1131
  cmd: ["npm", "run", "dev"],
1126
1132
  cwd: worktree.path,
@@ -1255,7 +1261,7 @@ async function vary(opts) {
1255
1261
  const port = await allocatePort2();
1256
1262
  const screenshotWorktree = await worktreeManager.createForGeneration(`screenshot-${stashId}`);
1257
1263
  const screenshotGit = simpleGit4(screenshotWorktree.path);
1258
- await screenshotGit.checkout(["-f", stash.branch]);
1264
+ await screenshotGit.checkout(["--detach", stash.branch]);
1259
1265
  const devServer = spawn4({
1260
1266
  cmd: ["npm", "run", "dev"],
1261
1267
  cwd: screenshotWorktree.path,
@@ -1537,7 +1543,10 @@ class StashService {
1537
1543
  selectedComponent = null;
1538
1544
  messageQueue = [];
1539
1545
  isProcessingMessage = false;
1546
+ activeChatId = null;
1540
1547
  chatSessions = new Map;
1548
+ stashPollTimer = null;
1549
+ knownStashIds = new Set;
1541
1550
  constructor(projectPath, worktreeManager, persistence, broadcast) {
1542
1551
  this.projectPath = projectPath;
1543
1552
  this.worktreeManager = worktreeManager;
@@ -1545,6 +1554,9 @@ class StashService {
1545
1554
  this.broadcast = broadcast;
1546
1555
  this.previewPool = new PreviewPool(worktreeManager, broadcast);
1547
1556
  }
1557
+ getActiveChatId() {
1558
+ return this.activeChatId;
1559
+ }
1548
1560
  setSelectedComponent(component) {
1549
1561
  this.selectedComponent = component;
1550
1562
  if (component.filePath === "auto-detect") {
@@ -1599,8 +1611,10 @@ class StashService {
1599
1611
  this.isProcessingMessage = true;
1600
1612
  while (this.messageQueue.length > 0) {
1601
1613
  const msg = this.messageQueue.shift();
1614
+ this.activeChatId = msg.chatId;
1602
1615
  await this.processMessage(msg.projectId, msg.chatId, msg.message, msg.referenceStashIds, msg.componentContext);
1603
1616
  }
1617
+ this.activeChatId = null;
1604
1618
  this.isProcessingMessage = false;
1605
1619
  }
1606
1620
  async processMessage(projectId, chatId, message, referenceStashIds, componentContext) {
@@ -1660,20 +1674,22 @@ ${sourceCode.substring(0, 3000)}
1660
1674
  const aiProcess = startAiProcess("chat", chatPrompt, this.projectPath, existingSessionId);
1661
1675
  let thinkingBuf = "";
1662
1676
  let textBuf = "";
1663
- const pendingMessages = [];
1664
1677
  const now = new Date().toISOString();
1665
- function flushThinking() {
1678
+ const save = (msg) => {
1679
+ this.persistence.saveChatMessage(projectId, chatId, msg);
1680
+ };
1681
+ const flushThinking = () => {
1666
1682
  if (!thinkingBuf)
1667
1683
  return;
1668
- pendingMessages.push({ id: crypto.randomUUID(), role: "assistant", content: thinkingBuf, type: "thinking", createdAt: now });
1684
+ save({ id: crypto.randomUUID(), role: "assistant", content: thinkingBuf, type: "thinking", createdAt: now });
1669
1685
  thinkingBuf = "";
1670
- }
1671
- function flushText() {
1686
+ };
1687
+ const flushText = () => {
1672
1688
  if (!textBuf)
1673
1689
  return;
1674
- pendingMessages.push({ id: crypto.randomUUID(), role: "assistant", content: textBuf, type: "text", createdAt: now });
1690
+ save({ id: crypto.randomUUID(), role: "assistant", content: textBuf, type: "text", createdAt: now });
1675
1691
  textBuf = "";
1676
- }
1692
+ };
1677
1693
  try {
1678
1694
  for await (const chunk of parseClaudeStream(aiProcess.process)) {
1679
1695
  if (chunk.type === "session_id" && chunk.sessionId) {
@@ -1707,7 +1723,7 @@ ${sourceCode.substring(0, 3000)}
1707
1723
  toolName = parsed.tool ?? "unknown";
1708
1724
  toolParams = parsed.input ?? {};
1709
1725
  } catch {}
1710
- pendingMessages.push({
1726
+ save({
1711
1727
  id: crypto.randomUUID(),
1712
1728
  role: "assistant",
1713
1729
  content: chunk.content,
@@ -1726,7 +1742,11 @@ ${sourceCode.substring(0, 3000)}
1726
1742
  toolParams,
1727
1743
  toolStatus: "running"
1728
1744
  });
1745
+ if (toolName.includes("stashes_generate") || toolName.includes("stashes_vary")) {
1746
+ this.startStashPoll(projectId, chatId);
1747
+ }
1729
1748
  } else if (chunk.type === "tool_result") {
1749
+ this.stopStashPoll();
1730
1750
  let toolResult = chunk.content;
1731
1751
  let isError = false;
1732
1752
  try {
@@ -1734,7 +1754,7 @@ ${sourceCode.substring(0, 3000)}
1734
1754
  toolResult = parsed.result ?? chunk.content;
1735
1755
  isError = !!parsed.is_error;
1736
1756
  } catch {}
1737
- pendingMessages.push({
1757
+ save({
1738
1758
  id: crypto.randomUUID(),
1739
1759
  role: "assistant",
1740
1760
  content: chunk.content,
@@ -1756,10 +1776,7 @@ ${sourceCode.substring(0, 3000)}
1756
1776
  await aiProcess.process.exited;
1757
1777
  flushThinking();
1758
1778
  flushText();
1759
- for (const msg of pendingMessages) {
1760
- this.persistence.saveChatMessage(projectId, chatId, msg);
1761
- }
1762
- this.syncStashesFromDisk(projectId);
1779
+ this.syncStashesFromDisk(projectId, chatId);
1763
1780
  } catch (err) {
1764
1781
  this.broadcast({
1765
1782
  type: "ai_stream",
@@ -1768,12 +1785,16 @@ ${sourceCode.substring(0, 3000)}
1768
1785
  source: "chat"
1769
1786
  });
1770
1787
  } finally {
1788
+ this.stopStashPoll();
1771
1789
  killAiProcess("chat");
1772
1790
  }
1773
1791
  }
1774
- syncStashesFromDisk(projectId) {
1792
+ syncStashesFromDisk(projectId, chatId) {
1775
1793
  const diskStashes = this.persistence.listStashes(projectId);
1776
1794
  for (const stash of diskStashes) {
1795
+ if (chatId && !stash.originChatId) {
1796
+ this.persistence.saveStash({ ...stash, originChatId: chatId });
1797
+ }
1777
1798
  this.broadcast({
1778
1799
  type: "stash:status",
1779
1800
  stashId: stash.id,
@@ -1788,6 +1809,48 @@ ${sourceCode.substring(0, 3000)}
1788
1809
  screenshots: stash.screenshots?.length ? [...stash.screenshots] : undefined
1789
1810
  });
1790
1811
  }
1812
+ this.knownStashIds.add(stash.id);
1813
+ }
1814
+ }
1815
+ startStashPoll(projectId, chatId) {
1816
+ this.stopStashPoll();
1817
+ const lastStatus = new Map;
1818
+ for (const s of this.persistence.listStashes(projectId)) {
1819
+ this.knownStashIds.add(s.id);
1820
+ lastStatus.set(s.id, s.status);
1821
+ }
1822
+ this.stashPollTimer = setInterval(() => {
1823
+ const stashes = this.persistence.listStashes(projectId);
1824
+ for (const stash of stashes) {
1825
+ const prev = lastStatus.get(stash.id);
1826
+ if (!prev || prev !== stash.status) {
1827
+ if (chatId && !stash.originChatId) {
1828
+ this.persistence.saveStash({ ...stash, originChatId: chatId });
1829
+ }
1830
+ this.broadcast({
1831
+ type: "stash:status",
1832
+ stashId: stash.id,
1833
+ status: stash.status,
1834
+ number: stash.number
1835
+ });
1836
+ if (stash.screenshotUrl && prev !== stash.status) {
1837
+ this.broadcast({
1838
+ type: "stash:screenshot",
1839
+ stashId: stash.id,
1840
+ url: stash.screenshotUrl,
1841
+ screenshots: stash.screenshots?.length ? [...stash.screenshots] : undefined
1842
+ });
1843
+ }
1844
+ lastStatus.set(stash.id, stash.status);
1845
+ this.knownStashIds.add(stash.id);
1846
+ }
1847
+ }
1848
+ }, 2000);
1849
+ }
1850
+ stopStashPoll() {
1851
+ if (this.stashPollTimer) {
1852
+ clearInterval(this.stashPollTimer);
1853
+ this.stashPollTimer = null;
1791
1854
  }
1792
1855
  }
1793
1856
  progressToBroadcast(event) {
@@ -1914,6 +1977,10 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
1914
1977
  projectId: project.id,
1915
1978
  projectName: project.name
1916
1979
  }));
1980
+ const activeChatId = stashService.getActiveChatId();
1981
+ if (activeChatId) {
1982
+ ws.send(JSON.stringify({ type: "processing", chatId: activeChatId }));
1983
+ }
1917
1984
  },
1918
1985
  async message(ws, message) {
1919
1986
  const raw = typeof message === "string" ? message : new TextDecoder().decode(message);
package/dist/mcp.js CHANGED
@@ -129,6 +129,10 @@ class WorktreeManager {
129
129
  const { readdirSync } = await import("fs");
130
130
  const entries = readdirSync(worktreesDir);
131
131
  for (const entry of entries) {
132
+ if (entry.startsWith("screenshot-")) {
133
+ logger.info("worktree", `skipping active screenshot worktree: ${entry}`);
134
+ continue;
135
+ }
132
136
  const worktreePath = join2(worktreesDir, entry);
133
137
  logger.info("worktree", `cleaning up stale worktree: ${entry}`);
134
138
  try {
@@ -778,6 +782,20 @@ function parseAiResult(text) {
778
782
  return null;
779
783
  }
780
784
  }
785
+ async function fallbackScreenshot(port, projectPath, stashId) {
786
+ try {
787
+ const url = await captureScreenshot(port, projectPath, stashId);
788
+ return {
789
+ primary: url,
790
+ screenshots: [{ url, label: "Homepage", route: "/", isPrimary: true }]
791
+ };
792
+ } catch (err) {
793
+ logger.error("smart-screenshot", `Fallback screenshot also failed for ${stashId}`, {
794
+ error: err instanceof Error ? err.message : String(err)
795
+ });
796
+ return { primary: "", screenshots: [] };
797
+ }
798
+ }
781
799
  async function captureSmartScreenshots(opts) {
782
800
  const { projectPath, stashId, stashBranch, parentBranch, worktreePath, port, model = "haiku", timeout = DEFAULT_TIMEOUT } = opts;
783
801
  const screenshotDir = join5(projectPath, SCREENSHOTS_DIR2);
@@ -787,11 +805,7 @@ async function captureSmartScreenshots(opts) {
787
805
  const diff = await getStashDiff(worktreePath, parentBranch);
788
806
  if (!diff) {
789
807
  logger.info("smart-screenshot", `No diff found for ${stashId}, using simple screenshot`);
790
- const url = await captureScreenshot(port, projectPath, stashId);
791
- return {
792
- primary: url,
793
- screenshots: [{ url, label: "Homepage", route: "/", isPrimary: true }]
794
- };
808
+ return fallbackScreenshot(port, projectPath, stashId);
795
809
  }
796
810
  const processId = `screenshot-ai-${stashId}`;
797
811
  const prompt = buildScreenshotPrompt(port, diff, screenshotDir, stashId);
@@ -821,12 +835,8 @@ async function captureSmartScreenshots(opts) {
821
835
  }
822
836
  const result = parseAiResult(textOutput);
823
837
  if (!result || !result.screenshots || result.screenshots.length === 0) {
824
- logger.info("smart-screenshot", `AI returned no screenshots for ${stashId}, falling back`);
825
- const url = await captureScreenshot(port, projectPath, stashId);
826
- return {
827
- primary: url,
828
- screenshots: [{ url, label: "Homepage", route: "/", isPrimary: true }]
829
- };
838
+ logger.info("smart-screenshot", `AI returned no screenshots for ${stashId} (timedOut=${timedOut}), falling back`);
839
+ return fallbackScreenshot(port, projectPath, stashId);
830
840
  }
831
841
  const screenshots = [];
832
842
  let primaryUrl = "";
@@ -849,11 +859,7 @@ async function captureSmartScreenshots(opts) {
849
859
  }
850
860
  if (screenshots.length === 0) {
851
861
  logger.info("smart-screenshot", `No valid screenshots for ${stashId}, falling back`);
852
- const url = await captureScreenshot(port, projectPath, stashId);
853
- return {
854
- primary: url,
855
- screenshots: [{ url, label: "Homepage", route: "/", isPrimary: true }]
856
- };
862
+ return fallbackScreenshot(port, projectPath, stashId);
857
863
  }
858
864
  if (!primaryUrl) {
859
865
  primaryUrl = screenshots[0].url;
@@ -1002,7 +1008,7 @@ async function generate(opts) {
1002
1008
  const port = await allocatePort();
1003
1009
  const worktree = await worktreeManager.createForGeneration(`screenshot-${stash.id}`);
1004
1010
  const screenshotGit = simpleGit3(worktree.path);
1005
- await screenshotGit.checkout(["-f", stash.branch]);
1011
+ await screenshotGit.checkout(["--detach", stash.branch]);
1006
1012
  const devServer = spawn3({
1007
1013
  cmd: ["npm", "run", "dev"],
1008
1014
  cwd: worktree.path,
@@ -1137,7 +1143,7 @@ async function vary(opts) {
1137
1143
  const port = await allocatePort2();
1138
1144
  const screenshotWorktree = await worktreeManager.createForGeneration(`screenshot-${stashId}`);
1139
1145
  const screenshotGit = simpleGit4(screenshotWorktree.path);
1140
- await screenshotGit.checkout(["-f", stash.branch]);
1146
+ await screenshotGit.checkout(["--detach", stash.branch]);
1141
1147
  const devServer = spawn4({
1142
1148
  cmd: ["npm", "run", "dev"],
1143
1149
  cwd: screenshotWorktree.path,
@@ -1733,7 +1739,10 @@ class StashService {
1733
1739
  selectedComponent = null;
1734
1740
  messageQueue = [];
1735
1741
  isProcessingMessage = false;
1742
+ activeChatId = null;
1736
1743
  chatSessions = new Map;
1744
+ stashPollTimer = null;
1745
+ knownStashIds = new Set;
1737
1746
  constructor(projectPath, worktreeManager, persistence, broadcast) {
1738
1747
  this.projectPath = projectPath;
1739
1748
  this.worktreeManager = worktreeManager;
@@ -1741,6 +1750,9 @@ class StashService {
1741
1750
  this.broadcast = broadcast;
1742
1751
  this.previewPool = new PreviewPool(worktreeManager, broadcast);
1743
1752
  }
1753
+ getActiveChatId() {
1754
+ return this.activeChatId;
1755
+ }
1744
1756
  setSelectedComponent(component) {
1745
1757
  this.selectedComponent = component;
1746
1758
  if (component.filePath === "auto-detect") {
@@ -1795,8 +1807,10 @@ class StashService {
1795
1807
  this.isProcessingMessage = true;
1796
1808
  while (this.messageQueue.length > 0) {
1797
1809
  const msg = this.messageQueue.shift();
1810
+ this.activeChatId = msg.chatId;
1798
1811
  await this.processMessage(msg.projectId, msg.chatId, msg.message, msg.referenceStashIds, msg.componentContext);
1799
1812
  }
1813
+ this.activeChatId = null;
1800
1814
  this.isProcessingMessage = false;
1801
1815
  }
1802
1816
  async processMessage(projectId, chatId, message, referenceStashIds, componentContext) {
@@ -1856,20 +1870,22 @@ ${sourceCode.substring(0, 3000)}
1856
1870
  const aiProcess = startAiProcess("chat", chatPrompt, this.projectPath, existingSessionId);
1857
1871
  let thinkingBuf = "";
1858
1872
  let textBuf = "";
1859
- const pendingMessages = [];
1860
1873
  const now = new Date().toISOString();
1861
- function flushThinking() {
1874
+ const save = (msg) => {
1875
+ this.persistence.saveChatMessage(projectId, chatId, msg);
1876
+ };
1877
+ const flushThinking = () => {
1862
1878
  if (!thinkingBuf)
1863
1879
  return;
1864
- pendingMessages.push({ id: crypto.randomUUID(), role: "assistant", content: thinkingBuf, type: "thinking", createdAt: now });
1880
+ save({ id: crypto.randomUUID(), role: "assistant", content: thinkingBuf, type: "thinking", createdAt: now });
1865
1881
  thinkingBuf = "";
1866
- }
1867
- function flushText() {
1882
+ };
1883
+ const flushText = () => {
1868
1884
  if (!textBuf)
1869
1885
  return;
1870
- pendingMessages.push({ id: crypto.randomUUID(), role: "assistant", content: textBuf, type: "text", createdAt: now });
1886
+ save({ id: crypto.randomUUID(), role: "assistant", content: textBuf, type: "text", createdAt: now });
1871
1887
  textBuf = "";
1872
- }
1888
+ };
1873
1889
  try {
1874
1890
  for await (const chunk of parseClaudeStream(aiProcess.process)) {
1875
1891
  if (chunk.type === "session_id" && chunk.sessionId) {
@@ -1903,7 +1919,7 @@ ${sourceCode.substring(0, 3000)}
1903
1919
  toolName = parsed.tool ?? "unknown";
1904
1920
  toolParams = parsed.input ?? {};
1905
1921
  } catch {}
1906
- pendingMessages.push({
1922
+ save({
1907
1923
  id: crypto.randomUUID(),
1908
1924
  role: "assistant",
1909
1925
  content: chunk.content,
@@ -1922,7 +1938,11 @@ ${sourceCode.substring(0, 3000)}
1922
1938
  toolParams,
1923
1939
  toolStatus: "running"
1924
1940
  });
1941
+ if (toolName.includes("stashes_generate") || toolName.includes("stashes_vary")) {
1942
+ this.startStashPoll(projectId, chatId);
1943
+ }
1925
1944
  } else if (chunk.type === "tool_result") {
1945
+ this.stopStashPoll();
1926
1946
  let toolResult = chunk.content;
1927
1947
  let isError = false;
1928
1948
  try {
@@ -1930,7 +1950,7 @@ ${sourceCode.substring(0, 3000)}
1930
1950
  toolResult = parsed.result ?? chunk.content;
1931
1951
  isError = !!parsed.is_error;
1932
1952
  } catch {}
1933
- pendingMessages.push({
1953
+ save({
1934
1954
  id: crypto.randomUUID(),
1935
1955
  role: "assistant",
1936
1956
  content: chunk.content,
@@ -1952,10 +1972,7 @@ ${sourceCode.substring(0, 3000)}
1952
1972
  await aiProcess.process.exited;
1953
1973
  flushThinking();
1954
1974
  flushText();
1955
- for (const msg of pendingMessages) {
1956
- this.persistence.saveChatMessage(projectId, chatId, msg);
1957
- }
1958
- this.syncStashesFromDisk(projectId);
1975
+ this.syncStashesFromDisk(projectId, chatId);
1959
1976
  } catch (err) {
1960
1977
  this.broadcast({
1961
1978
  type: "ai_stream",
@@ -1964,12 +1981,16 @@ ${sourceCode.substring(0, 3000)}
1964
1981
  source: "chat"
1965
1982
  });
1966
1983
  } finally {
1984
+ this.stopStashPoll();
1967
1985
  killAiProcess("chat");
1968
1986
  }
1969
1987
  }
1970
- syncStashesFromDisk(projectId) {
1988
+ syncStashesFromDisk(projectId, chatId) {
1971
1989
  const diskStashes = this.persistence.listStashes(projectId);
1972
1990
  for (const stash of diskStashes) {
1991
+ if (chatId && !stash.originChatId) {
1992
+ this.persistence.saveStash({ ...stash, originChatId: chatId });
1993
+ }
1973
1994
  this.broadcast({
1974
1995
  type: "stash:status",
1975
1996
  stashId: stash.id,
@@ -1984,6 +2005,48 @@ ${sourceCode.substring(0, 3000)}
1984
2005
  screenshots: stash.screenshots?.length ? [...stash.screenshots] : undefined
1985
2006
  });
1986
2007
  }
2008
+ this.knownStashIds.add(stash.id);
2009
+ }
2010
+ }
2011
+ startStashPoll(projectId, chatId) {
2012
+ this.stopStashPoll();
2013
+ const lastStatus = new Map;
2014
+ for (const s of this.persistence.listStashes(projectId)) {
2015
+ this.knownStashIds.add(s.id);
2016
+ lastStatus.set(s.id, s.status);
2017
+ }
2018
+ this.stashPollTimer = setInterval(() => {
2019
+ const stashes = this.persistence.listStashes(projectId);
2020
+ for (const stash of stashes) {
2021
+ const prev = lastStatus.get(stash.id);
2022
+ if (!prev || prev !== stash.status) {
2023
+ if (chatId && !stash.originChatId) {
2024
+ this.persistence.saveStash({ ...stash, originChatId: chatId });
2025
+ }
2026
+ this.broadcast({
2027
+ type: "stash:status",
2028
+ stashId: stash.id,
2029
+ status: stash.status,
2030
+ number: stash.number
2031
+ });
2032
+ if (stash.screenshotUrl && prev !== stash.status) {
2033
+ this.broadcast({
2034
+ type: "stash:screenshot",
2035
+ stashId: stash.id,
2036
+ url: stash.screenshotUrl,
2037
+ screenshots: stash.screenshots?.length ? [...stash.screenshots] : undefined
2038
+ });
2039
+ }
2040
+ lastStatus.set(stash.id, stash.status);
2041
+ this.knownStashIds.add(stash.id);
2042
+ }
2043
+ }
2044
+ }, 2000);
2045
+ }
2046
+ stopStashPoll() {
2047
+ if (this.stashPollTimer) {
2048
+ clearInterval(this.stashPollTimer);
2049
+ this.stashPollTimer = null;
1987
2050
  }
1988
2051
  }
1989
2052
  progressToBroadcast(event) {
@@ -2110,6 +2173,10 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
2110
2173
  projectId: project.id,
2111
2174
  projectName: project.name
2112
2175
  }));
2176
+ const activeChatId = stashService.getActiveChatId();
2177
+ if (activeChatId) {
2178
+ ws.send(JSON.stringify({ type: "processing", chatId: activeChatId }));
2179
+ }
2113
2180
  },
2114
2181
  async message(ws, message) {
2115
2182
  const raw = typeof message === "string" ? message : new TextDecoder().decode(message);