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 +99 -32
- package/dist/mcp.js +99 -32
- package/dist/web/assets/{index-DFTLJjq2.js → index-CBnXcjSs.js} +8 -8
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(["
|
|
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(["
|
|
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
|
-
|
|
1678
|
+
const save = (msg) => {
|
|
1679
|
+
this.persistence.saveChatMessage(projectId, chatId, msg);
|
|
1680
|
+
};
|
|
1681
|
+
const flushThinking = () => {
|
|
1666
1682
|
if (!thinkingBuf)
|
|
1667
1683
|
return;
|
|
1668
|
-
|
|
1684
|
+
save({ id: crypto.randomUUID(), role: "assistant", content: thinkingBuf, type: "thinking", createdAt: now });
|
|
1669
1685
|
thinkingBuf = "";
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1686
|
+
};
|
|
1687
|
+
const flushText = () => {
|
|
1672
1688
|
if (!textBuf)
|
|
1673
1689
|
return;
|
|
1674
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(["
|
|
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(["
|
|
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
|
-
|
|
1874
|
+
const save = (msg) => {
|
|
1875
|
+
this.persistence.saveChatMessage(projectId, chatId, msg);
|
|
1876
|
+
};
|
|
1877
|
+
const flushThinking = () => {
|
|
1862
1878
|
if (!thinkingBuf)
|
|
1863
1879
|
return;
|
|
1864
|
-
|
|
1880
|
+
save({ id: crypto.randomUUID(), role: "assistant", content: thinkingBuf, type: "thinking", createdAt: now });
|
|
1865
1881
|
thinkingBuf = "";
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1882
|
+
};
|
|
1883
|
+
const flushText = () => {
|
|
1868
1884
|
if (!textBuf)
|
|
1869
1885
|
return;
|
|
1870
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|