stashes 0.1.29 → 0.1.31

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,
@@ -1612,6 +1618,7 @@ class StashService {
1612
1618
  this.isProcessingMessage = false;
1613
1619
  }
1614
1620
  async processMessage(projectId, chatId, message, referenceStashIds, componentContext) {
1621
+ const preExistingStashIds = new Set(this.persistence.listStashes(projectId).map((s) => s.id));
1615
1622
  const component = componentContext ? { name: componentContext.name, filePath: this.selectedComponent?.filePath || "" } : this.selectedComponent;
1616
1623
  let sourceCode = "";
1617
1624
  const filePath = component?.filePath || "";
@@ -1770,7 +1777,7 @@ ${sourceCode.substring(0, 3000)}
1770
1777
  await aiProcess.process.exited;
1771
1778
  flushThinking();
1772
1779
  flushText();
1773
- this.syncStashesFromDisk(projectId, chatId);
1780
+ this.syncStashesFromDisk(projectId, chatId, preExistingStashIds);
1774
1781
  } catch (err) {
1775
1782
  this.broadcast({
1776
1783
  type: "ai_stream",
@@ -1783,12 +1790,11 @@ ${sourceCode.substring(0, 3000)}
1783
1790
  killAiProcess("chat");
1784
1791
  }
1785
1792
  }
1786
- syncStashesFromDisk(projectId, chatId) {
1793
+ syncStashesFromDisk(projectId, chatId, preExistingIds) {
1787
1794
  const diskStashes = this.persistence.listStashes(projectId);
1788
1795
  for (const stash of diskStashes) {
1789
- if (chatId && !stash.originChatId && !this.knownStashIds.has(stash.id)) {
1790
- const updated = { ...stash, originChatId: chatId };
1791
- this.persistence.saveStash(updated);
1796
+ if (chatId && !stash.originChatId && preExistingIds && !preExistingIds.has(stash.id)) {
1797
+ this.persistence.saveStash({ ...stash, originChatId: chatId });
1792
1798
  }
1793
1799
  this.broadcast({
1794
1800
  type: "stash:status",
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,
@@ -1808,6 +1814,7 @@ class StashService {
1808
1814
  this.isProcessingMessage = false;
1809
1815
  }
1810
1816
  async processMessage(projectId, chatId, message, referenceStashIds, componentContext) {
1817
+ const preExistingStashIds = new Set(this.persistence.listStashes(projectId).map((s) => s.id));
1811
1818
  const component = componentContext ? { name: componentContext.name, filePath: this.selectedComponent?.filePath || "" } : this.selectedComponent;
1812
1819
  let sourceCode = "";
1813
1820
  const filePath = component?.filePath || "";
@@ -1966,7 +1973,7 @@ ${sourceCode.substring(0, 3000)}
1966
1973
  await aiProcess.process.exited;
1967
1974
  flushThinking();
1968
1975
  flushText();
1969
- this.syncStashesFromDisk(projectId, chatId);
1976
+ this.syncStashesFromDisk(projectId, chatId, preExistingStashIds);
1970
1977
  } catch (err) {
1971
1978
  this.broadcast({
1972
1979
  type: "ai_stream",
@@ -1979,12 +1986,11 @@ ${sourceCode.substring(0, 3000)}
1979
1986
  killAiProcess("chat");
1980
1987
  }
1981
1988
  }
1982
- syncStashesFromDisk(projectId, chatId) {
1989
+ syncStashesFromDisk(projectId, chatId, preExistingIds) {
1983
1990
  const diskStashes = this.persistence.listStashes(projectId);
1984
1991
  for (const stash of diskStashes) {
1985
- if (chatId && !stash.originChatId && !this.knownStashIds.has(stash.id)) {
1986
- const updated = { ...stash, originChatId: chatId };
1987
- this.persistence.saveStash(updated);
1992
+ if (chatId && !stash.originChatId && preExistingIds && !preExistingIds.has(stash.id)) {
1993
+ this.persistence.saveStash({ ...stash, originChatId: chatId });
1988
1994
  }
1989
1995
  this.broadcast({
1990
1996
  type: "stash:status",