@ottocode/web-sdk 0.1.272 → 0.1.274

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.
Files changed (100) hide show
  1. package/dist/components/branch/BranchModal.d.ts.map +1 -1
  2. package/dist/components/chat/ChatInputContainer.d.ts +1 -0
  3. package/dist/components/chat/ChatInputContainer.d.ts.map +1 -1
  4. package/dist/components/chat/ConfigSelector.d.ts.map +1 -1
  5. package/dist/components/file-browser/FileBrowserSidebar.d.ts.map +1 -1
  6. package/dist/components/file-browser/FileViewerPanel.d.ts +4 -0
  7. package/dist/components/file-browser/FileViewerPanel.d.ts.map +1 -1
  8. package/dist/components/git/GitCommitModal.d.ts.map +1 -1
  9. package/dist/components/git/GitDiffViewer.d.ts.map +1 -1
  10. package/dist/components/git/GitFileList.d.ts.map +1 -1
  11. package/dist/components/git/GitSidebar.d.ts.map +1 -1
  12. package/dist/components/index.d.ts +1 -0
  13. package/dist/components/index.d.ts.map +1 -1
  14. package/dist/components/index.js +5927 -4065
  15. package/dist/components/index.js.map +57 -52
  16. package/dist/components/mcp/AddMCPServerModal.d.ts.map +1 -1
  17. package/dist/components/mcp/MCPSidebar.d.ts.map +1 -1
  18. package/dist/components/messages/ActionToolBox.d.ts.map +1 -1
  19. package/dist/components/messages/AssistantMessageGroup.d.ts.map +1 -1
  20. package/dist/components/messages/CompactActivityGroup.d.ts.map +1 -1
  21. package/dist/components/messages/MessagePartItem.d.ts.map +1 -1
  22. package/dist/components/messages/MessageThread.d.ts.map +1 -1
  23. package/dist/components/messages/TopupApprovalCard.d.ts.map +1 -1
  24. package/dist/components/messages/compactActivity.d.ts.map +1 -1
  25. package/dist/components/messages/renderers/CopyIntoRenderer.d.ts +3 -0
  26. package/dist/components/messages/renderers/CopyIntoRenderer.d.ts.map +1 -0
  27. package/dist/components/messages/renderers/DatabaseToolRenderer.d.ts.map +1 -1
  28. package/dist/components/messages/renderers/index.d.ts.map +1 -1
  29. package/dist/components/messages/renderers/types.d.ts +22 -0
  30. package/dist/components/messages/renderers/types.d.ts.map +1 -1
  31. package/dist/components/onboarding/steps/DefaultsStep.d.ts.map +1 -1
  32. package/dist/components/onboarding/steps/ProviderSetupStep.d.ts.map +1 -1
  33. package/dist/components/session-files/SessionFilesDiffPanel.d.ts.map +1 -1
  34. package/dist/components/session-files/SessionFilesSidebar.d.ts.map +1 -1
  35. package/dist/components/sessions/LeanHeader.d.ts.map +1 -1
  36. package/dist/components/sessions/SessionHeader.d.ts.map +1 -1
  37. package/dist/components/sessions/SessionItem.d.ts.map +1 -1
  38. package/dist/components/sessions/SessionListContainer.d.ts.map +1 -1
  39. package/dist/components/settings/OttoRouterTopupModal.d.ts.map +1 -1
  40. package/dist/components/skills/SkillViewerPanel.d.ts.map +1 -1
  41. package/dist/components/skills/SkillsSidebar.d.ts.map +1 -1
  42. package/dist/components/terminals/TerminalViewer.d.ts.map +1 -1
  43. package/dist/components/tunnel/TunnelSidebar.d.ts.map +1 -1
  44. package/dist/components/ui/CodeMirrorViewer.d.ts +14 -0
  45. package/dist/components/ui/CodeMirrorViewer.d.ts.map +1 -0
  46. package/dist/components/ui/StableSpinner.d.ts +15 -0
  47. package/dist/components/ui/StableSpinner.d.ts.map +1 -0
  48. package/dist/components/ui/Toaster.d.ts.map +1 -1
  49. package/dist/components/workspace/ToolActivityToggle.d.ts +6 -0
  50. package/dist/components/workspace/ToolActivityToggle.d.ts.map +1 -0
  51. package/dist/components/workspace/ToolPreviewPanel.d.ts +17 -0
  52. package/dist/components/workspace/ToolPreviewPanel.d.ts.map +1 -0
  53. package/dist/components/workspace/ViewerTabs.d.ts.map +1 -1
  54. package/dist/hooks/index.js +720 -78
  55. package/dist/hooks/index.js.map +13 -13
  56. package/dist/hooks/useBranch.d.ts +2 -2
  57. package/dist/hooks/useBranch.d.ts.map +1 -1
  58. package/dist/hooks/useClientEvents.d.ts.map +1 -1
  59. package/dist/hooks/useConfig.d.ts +5 -5
  60. package/dist/hooks/useConfig.d.ts.map +1 -1
  61. package/dist/hooks/useFileBrowser.d.ts +5 -5
  62. package/dist/hooks/useFileBrowser.d.ts.map +1 -1
  63. package/dist/hooks/useFiles.d.ts +2 -2
  64. package/dist/hooks/useFiles.d.ts.map +1 -1
  65. package/dist/hooks/useGit.d.ts +5 -4
  66. package/dist/hooks/useGit.d.ts.map +1 -1
  67. package/dist/hooks/useMCP.d.ts +3 -3
  68. package/dist/hooks/useMCP.d.ts.map +1 -1
  69. package/dist/hooks/useMessages.d.ts +1 -1
  70. package/dist/hooks/useMessages.d.ts.map +1 -1
  71. package/dist/hooks/useQueueState.d.ts +2 -0
  72. package/dist/hooks/useQueueState.d.ts.map +1 -1
  73. package/dist/hooks/useResearch.d.ts +1 -1
  74. package/dist/hooks/useResearch.d.ts.map +1 -1
  75. package/dist/hooks/useSessionFiles.d.ts +1 -1
  76. package/dist/hooks/useSessionFiles.d.ts.map +1 -1
  77. package/dist/hooks/useSessionStream.d.ts.map +1 -1
  78. package/dist/hooks/useSkills.d.ts +6 -6
  79. package/dist/hooks/useSkills.d.ts.map +1 -1
  80. package/dist/hooks/useTerminals.d.ts +2 -2
  81. package/dist/hooks/useTerminals.d.ts.map +1 -1
  82. package/dist/hooks/useTunnel.d.ts +2 -2
  83. package/dist/hooks/useTunnel.d.ts.map +1 -1
  84. package/dist/index.js +5943 -4075
  85. package/dist/index.js.map +59 -54
  86. package/dist/lib/api-client/git.d.ts +2 -1
  87. package/dist/lib/api-client/git.d.ts.map +1 -1
  88. package/dist/lib/api-client/index.d.ts +1 -0
  89. package/dist/lib/api-client/index.d.ts.map +1 -1
  90. package/dist/lib/index.js +21 -2
  91. package/dist/lib/index.js.map +5 -5
  92. package/dist/lib/platform.d.ts +1 -0
  93. package/dist/lib/platform.d.ts.map +1 -1
  94. package/dist/stores/index.js +159 -11
  95. package/dist/stores/index.js.map +3 -3
  96. package/dist/stores/viewerTabsStore.d.ts +55 -0
  97. package/dist/stores/viewerTabsStore.d.ts.map +1 -1
  98. package/dist/types/api.d.ts +16 -0
  99. package/dist/types/api.d.ts.map +1 -1
  100. package/package.json +17 -3
@@ -228,6 +228,7 @@ import {
228
228
  generateCommitMessage as apiGenerateCommitMessage,
229
229
  pushCommits as apiPushCommits,
230
230
  pullChanges as apiPullChanges,
231
+ performGitRebaseAction as apiPerformGitRebaseAction,
231
232
  initGitRepo as apiInitGitRepo,
232
233
  getGitRemotes as apiGetGitRemotes,
233
234
  addGitRemote as apiAddGitRemote,
@@ -235,7 +236,9 @@ import {
235
236
  } from "@ottocode/api";
236
237
  var gitMixin = {
237
238
  async initGitRepo() {
238
- const response = await apiInitGitRepo();
239
+ const response = await apiInitGitRepo({
240
+ body: {}
241
+ });
239
242
  if (response.error)
240
243
  throw new Error(extractErrorMessage(response.error));
241
244
  return response.data?.data;
@@ -336,6 +339,14 @@ var gitMixin = {
336
339
  throw new Error(extractErrorMessage(response.error));
337
340
  return response.data?.data;
338
341
  },
342
+ async performRebaseAction(action) {
343
+ const response = await apiPerformGitRebaseAction({
344
+ body: { action }
345
+ });
346
+ if (response.error)
347
+ throw new Error(extractErrorMessage(response.error));
348
+ return response.data?.data;
349
+ },
339
350
  async getRemotes() {
340
351
  const response = await apiGetGitRemotes();
341
352
  if (response.error)
@@ -903,6 +914,7 @@ class ApiClient {
903
914
  getGitBranch = gitMixin.getGitBranch;
904
915
  pushCommits = gitMixin.pushCommits;
905
916
  pullChanges = gitMixin.pullChanges;
917
+ performRebaseAction = gitMixin.performRebaseAction;
906
918
  getRemotes = gitMixin.getRemotes;
907
919
  addRemote = gitMixin.addRemote;
908
920
  removeRemote = gitMixin.removeRemote;
@@ -1057,6 +1069,12 @@ function openPlatformSession(sessionId) {
1057
1069
  win.OTTO_OPEN_SESSION(sessionId);
1058
1070
  return true;
1059
1071
  }
1072
+ function getPlatformWindowFocused() {
1073
+ const win = getPlatformWindow();
1074
+ if (!win?.OTTO_IS_WINDOW_FOCUSED)
1075
+ return null;
1076
+ return win.OTTO_IS_WINDOW_FOCUSED();
1077
+ }
1060
1078
  function hasPlatformOpenUrl() {
1061
1079
  return !!getPlatformWindow()?.OTTO_OPEN_URL;
1062
1080
  }
@@ -1221,6 +1239,17 @@ import { create } from "zustand";
1221
1239
  function titleFromPath(path) {
1222
1240
  return path.split("/").pop() || path;
1223
1241
  }
1242
+ function normalizeViewerPath(path) {
1243
+ return path.trim().replace(/^a\//, "").replace(/^b\//, "").replace(/^\.\//, "").replace(/\/+/g, "/").replace(/\/+$/, "");
1244
+ }
1245
+ function viewerPathsMatch(left, right) {
1246
+ const normalizedLeft = normalizeViewerPath(left);
1247
+ const normalizedRight = normalizeViewerPath(right);
1248
+ return normalizedLeft === normalizedRight || normalizedLeft.endsWith(`/${normalizedRight}`) || normalizedRight.endsWith(`/${normalizedLeft}`);
1249
+ }
1250
+ function fileTabId(path) {
1251
+ return `file:${normalizeViewerPath(path)}`;
1252
+ }
1224
1253
  function upsertTab(tabs, tab) {
1225
1254
  const existingIndex = tabs.findIndex((item) => item.id === tab.id);
1226
1255
  if (existingIndex === -1) {
@@ -1230,9 +1259,32 @@ function upsertTab(tabs, tab) {
1230
1259
  next[existingIndex] = tab;
1231
1260
  return next;
1232
1261
  }
1262
+ function isSamePatchCall(existing, preview) {
1263
+ if (!existing)
1264
+ return false;
1265
+ if (preview.callId || existing.callId) {
1266
+ return Boolean(preview.callId && existing.callId === preview.callId);
1267
+ }
1268
+ if (preview.status === "streaming" && existing.status !== "streaming") {
1269
+ return false;
1270
+ }
1271
+ if (!preview.patch || !existing.patch)
1272
+ return existing.status === "streaming";
1273
+ return preview.patch === existing.patch || preview.patch.startsWith(existing.patch) || existing.patch.startsWith(preview.patch);
1274
+ }
1275
+ function mergeChangedLines(existing, incoming) {
1276
+ if (!existing?.length)
1277
+ return incoming;
1278
+ if (!incoming?.length)
1279
+ return existing;
1280
+ return [...new Set([...existing, ...incoming])].sort((a, b) => a - b);
1281
+ }
1233
1282
  var useViewerTabsStore = create((set) => ({
1234
1283
  tabs: [],
1235
1284
  activeTabId: null,
1285
+ followToolActivity: false,
1286
+ toggleFollowToolActivity: () => set((state) => ({ followToolActivity: !state.followToolActivity })),
1287
+ setFollowToolActivity: (enabled) => set({ followToolActivity: enabled }),
1236
1288
  openGitDiffTab: (path, staged) => {
1237
1289
  const id = `git-diff:${staged ? "staged" : "unstaged"}:${path}`;
1238
1290
  set((state) => ({
@@ -1261,16 +1313,130 @@ var useViewerTabsStore = create((set) => ({
1261
1313
  }));
1262
1314
  },
1263
1315
  openFileTab: (path) => {
1264
- const id = `file:${path}`;
1265
- set((state) => ({
1266
- tabs: upsertTab(state.tabs, {
1267
- id,
1268
- type: "file",
1269
- title: titleFromPath(path),
1270
- path
1271
- }),
1272
- activeTabId: id
1273
- }));
1316
+ const id = fileTabId(path);
1317
+ set((state) => {
1318
+ const matchingFileTabs = state.tabs.filter((tab) => tab.type === "file" && viewerPathsMatch(tab.path, path));
1319
+ const existingFile = matchingFileTabs.find((tab) => tab.id === state.activeTabId) ?? matchingFileTabs[0];
1320
+ const targetId = existingFile?.id ?? id;
1321
+ const targetPath = existingFile?.path ?? path;
1322
+ const tabs = state.tabs.filter((tab) => !((tab.type === "tool-preview" || tab.type === "file") && tab.id !== targetId && viewerPathsMatch(tab.path, path)));
1323
+ return {
1324
+ tabs: upsertTab(tabs, {
1325
+ id: targetId,
1326
+ type: "file",
1327
+ title: existingFile?.title ?? titleFromPath(targetPath),
1328
+ path: targetPath
1329
+ }),
1330
+ activeTabId: targetId
1331
+ };
1332
+ });
1333
+ },
1334
+ openToolReadTab: (path, highlight) => {
1335
+ const id = fileTabId(path);
1336
+ set((state) => {
1337
+ const matchingFileTabs = state.tabs.filter((tab) => tab.type === "file" && viewerPathsMatch(tab.path, path));
1338
+ const existingFile = matchingFileTabs.find((tab) => tab.id === state.activeTabId) ?? matchingFileTabs[0];
1339
+ const targetId = existingFile?.id ?? id;
1340
+ const targetPath = existingFile?.path ?? path;
1341
+ const tabs = state.tabs.filter((tab) => !((tab.type === "tool-preview" || tab.type === "file") && tab.id !== targetId && viewerPathsMatch(tab.path, path)));
1342
+ return {
1343
+ tabs: upsertTab(tabs, {
1344
+ id: targetId,
1345
+ type: "file",
1346
+ title: existingFile?.title ?? titleFromPath(targetPath),
1347
+ path: targetPath,
1348
+ highlight,
1349
+ patchPreview: undefined,
1350
+ writePreview: undefined
1351
+ }),
1352
+ activeTabId: targetId
1353
+ };
1354
+ });
1355
+ },
1356
+ openToolPreviewTab: (preview) => {
1357
+ const id = fileTabId(preview.path);
1358
+ set((state) => {
1359
+ const matchingFileTabs = state.tabs.filter((tab) => tab.type === "file" && viewerPathsMatch(tab.path, preview.path));
1360
+ const existingFile = matchingFileTabs.find((tab) => tab.id === state.activeTabId) ?? matchingFileTabs[0];
1361
+ const targetId = existingFile?.id ?? id;
1362
+ const targetPath = existingFile?.path ?? preview.path;
1363
+ const existing = state.tabs.find((tab) => tab.type === "tool-preview" && viewerPathsMatch(tab.path, preview.path));
1364
+ const tabs = state.tabs.filter((tab) => !((tab.type === "tool-preview" || tab.type === "file") && tab.id !== targetId && (viewerPathsMatch(tab.path, preview.path) || Boolean(preview.callId && "toolName" in tab && tab.callId === preview.callId))));
1365
+ if (preview.toolName === "apply_patch") {
1366
+ const existingPatchPreview = existingFile?.patchPreview ?? (existing?.toolName === "apply_patch" ? existing : undefined);
1367
+ const samePatchCall = isSamePatchCall(existingPatchPreview, preview);
1368
+ const baseContent = preview.baseContent ?? (samePatchCall ? existingPatchPreview?.baseContent : existingPatchPreview?.resultContent ?? existingPatchPreview?.baseContent);
1369
+ const changedLines = samePatchCall ? preview.changedLines ?? existingPatchPreview?.changedLines : mergeChangedLines(existingPatchPreview?.changedLines, preview.changedLines);
1370
+ return {
1371
+ tabs: upsertTab(tabs, {
1372
+ id: targetId,
1373
+ type: "file",
1374
+ title: existingFile?.title ?? titleFromPath(targetPath),
1375
+ path: targetPath,
1376
+ highlight: undefined,
1377
+ writePreview: undefined,
1378
+ patchPreview: {
1379
+ path: targetPath,
1380
+ toolName: "apply_patch",
1381
+ callId: preview.callId ?? existingPatchPreview?.callId,
1382
+ baseContent,
1383
+ patch: preview.patch ?? existingPatchPreview?.patch,
1384
+ changedLines,
1385
+ previewContent: preview.previewContent ?? (samePatchCall ? existingPatchPreview?.previewContent : undefined),
1386
+ resultContent: preview.resultContent ?? (samePatchCall ? existingPatchPreview?.resultContent : undefined),
1387
+ previewLineTones: preview.previewLineTones ?? (samePatchCall ? existingPatchPreview?.previewLineTones : undefined),
1388
+ previewFirstLine: preview.previewFirstLine ?? (samePatchCall ? existingPatchPreview?.previewFirstLine : undefined),
1389
+ previewLatestLine: preview.previewLatestLine ?? (samePatchCall ? existingPatchPreview?.previewLatestLine : undefined),
1390
+ status: preview.status,
1391
+ error: preview.error ?? existingPatchPreview?.error
1392
+ }
1393
+ }),
1394
+ activeTabId: targetId
1395
+ };
1396
+ }
1397
+ if (existingFile) {
1398
+ const existingWritePreview = existingFile.writePreview;
1399
+ return {
1400
+ tabs: upsertTab(tabs, {
1401
+ ...existingFile,
1402
+ highlight: undefined,
1403
+ patchPreview: undefined,
1404
+ writePreview: {
1405
+ path: targetPath,
1406
+ toolName: "write",
1407
+ callId: preview.callId ?? existingWritePreview?.callId,
1408
+ content: preview.content ?? existingWritePreview?.content,
1409
+ status: preview.status,
1410
+ error: preview.error ?? existingWritePreview?.error
1411
+ }
1412
+ }),
1413
+ activeTabId: targetId
1414
+ };
1415
+ }
1416
+ const existingWrite = existing?.toolName === "write" ? existing : undefined;
1417
+ return {
1418
+ tabs: upsertTab(tabs, {
1419
+ id,
1420
+ type: "tool-preview",
1421
+ title: titleFromPath(preview.path),
1422
+ path: preview.path,
1423
+ toolName: preview.toolName,
1424
+ callId: preview.callId,
1425
+ content: preview.content ?? existingWrite?.content,
1426
+ baseContent: undefined,
1427
+ patch: undefined,
1428
+ changedLines: undefined,
1429
+ previewContent: undefined,
1430
+ resultContent: undefined,
1431
+ previewLineTones: undefined,
1432
+ previewFirstLine: undefined,
1433
+ previewLatestLine: undefined,
1434
+ status: preview.status,
1435
+ error: preview.error ?? existingWrite?.error
1436
+ }),
1437
+ activeTabId: id
1438
+ };
1439
+ });
1274
1440
  },
1275
1441
  openSkillFileTab: (skill, file) => {
1276
1442
  const displayFile = file ?? "SKILL.md";
@@ -1811,6 +1977,16 @@ function usePullChanges() {
1811
1977
  }
1812
1978
  });
1813
1979
  }
1980
+ function useGitRebaseAction() {
1981
+ const queryClient = useQueryClient2();
1982
+ return useMutation2({
1983
+ mutationFn: (action) => apiClient.performRebaseAction(action),
1984
+ onSuccess: () => {
1985
+ queryClient.invalidateQueries({ queryKey: ["git", "status"] });
1986
+ queryClient.invalidateQueries({ queryKey: ["git", "branch"] });
1987
+ }
1988
+ });
1989
+ }
1814
1990
  function useGitInit() {
1815
1991
  const queryClient = useQueryClient2();
1816
1992
  return useMutation2({
@@ -1852,7 +2028,71 @@ function useRemoveRemote() {
1852
2028
  });
1853
2029
  }
1854
2030
  // src/hooks/useMessages.ts
1855
- import { useQuery as useQuery4, useMutation as useMutation4, useQueryClient as useQueryClient4 } from "@tanstack/react-query";
2031
+ import { useQuery as useQuery5, useMutation as useMutation4, useQueryClient as useQueryClient4 } from "@tanstack/react-query";
2032
+
2033
+ // src/hooks/useQueueState.ts
2034
+ import { useQuery as useQuery4 } from "@tanstack/react-query";
2035
+ var defaultQueueState = {
2036
+ currentMessageId: null,
2037
+ queuedMessages: [],
2038
+ queueLength: 0
2039
+ };
2040
+ function optimisticallyQueueMessage(queryClient, sessionId, messageId) {
2041
+ queryClient.setQueryData(["queueState", sessionId], (current) => {
2042
+ if (!current)
2043
+ return current;
2044
+ const isBusy = Boolean(current.currentMessageId) || current.queuedMessages.length > 0 || current.queueLength > 0;
2045
+ if (!isBusy)
2046
+ return current;
2047
+ if (current.currentMessageId === messageId)
2048
+ return current;
2049
+ if (current.queuedMessages.some((item) => item.messageId === messageId)) {
2050
+ return current;
2051
+ }
2052
+ const queuedMessages = [
2053
+ ...current.queuedMessages,
2054
+ { messageId, position: current.queuedMessages.length }
2055
+ ];
2056
+ return {
2057
+ ...current,
2058
+ queuedMessages,
2059
+ queueLength: queuedMessages.length
2060
+ };
2061
+ });
2062
+ }
2063
+ function useQueueState(sessionId) {
2064
+ const { data } = useQuery4({
2065
+ queryKey: ["queueState", sessionId],
2066
+ queryFn: async () => {
2067
+ if (!sessionId)
2068
+ return defaultQueueState;
2069
+ const queueState = await apiClient.getQueueState(sessionId);
2070
+ return {
2071
+ currentMessageId: queueState.currentMessageId,
2072
+ queuedMessages: queueState.queuedMessages,
2073
+ queueLength: queueState.queuedMessages.length
2074
+ };
2075
+ },
2076
+ enabled: !!sessionId,
2077
+ placeholderData: defaultQueueState,
2078
+ staleTime: Infinity
2079
+ });
2080
+ return data ?? defaultQueueState;
2081
+ }
2082
+ function useMessageQueuePosition(sessionId, messageId) {
2083
+ const queueState = useQueueState(sessionId);
2084
+ if (!sessionId || !queueState) {
2085
+ return { isQueued: false, isRunning: false, position: null };
2086
+ }
2087
+ if (queueState.currentMessageId === messageId) {
2088
+ return { isQueued: false, isRunning: true, position: null };
2089
+ }
2090
+ const queuedItem = queueState.queuedMessages.find((item) => item.messageId === messageId);
2091
+ if (queuedItem) {
2092
+ return { isQueued: true, isRunning: false, position: queuedItem.position };
2093
+ }
2094
+ return { isQueued: false, isRunning: false, position: null };
2095
+ }
1856
2096
 
1857
2097
  // src/hooks/useSessions.ts
1858
2098
  import {
@@ -1963,7 +2203,7 @@ function useDeleteSession() {
1963
2203
  // src/hooks/useMessages.ts
1964
2204
  function useMessages(sessionId, options = {}) {
1965
2205
  const { enabled = true, staleTime = 15000 } = options;
1966
- return useQuery4({
2206
+ return useQuery5({
1967
2207
  queryKey: ["messages", sessionId],
1968
2208
  queryFn: () => {
1969
2209
  if (!sessionId) {
@@ -1985,7 +2225,8 @@ function useSendMessage(sessionId) {
1985
2225
  });
1986
2226
  return apiClient.sendMessage(sessionId, data);
1987
2227
  },
1988
- onSuccess: () => {
2228
+ onSuccess: (result) => {
2229
+ optimisticallyQueueMessage(queryClient, sessionId, result.messageId);
1989
2230
  queryClient.invalidateQueries({ queryKey: ["messages", sessionId] });
1990
2231
  queryClient.invalidateQueries({ queryKey: sessionsQueryKey });
1991
2232
  }
@@ -2125,10 +2366,22 @@ var useToolApprovalStore = create10((set) => ({
2125
2366
  }));
2126
2367
 
2127
2368
  // src/hooks/useSessionStream.ts
2369
+ var TOOL_PREVIEW_THROTTLE_MS = 500;
2370
+ var TOOL_PREVIEW_THROTTLE_MIN_CHARS = 8000;
2371
+ var TOOL_PREVIEW_THROTTLE_MIN_DELTA_CHARS = 16000;
2372
+ var STREAMING_WRITE_CONTENT_PREVIEW_CHARS = 24000;
2373
+ var STREAMING_PATCH_PREVIEW_HEAD_CHARS = 12000;
2374
+ var STREAMING_PATCH_PREVIEW_TAIL_CHARS = 24000;
2375
+ var STREAMING_TOOL_INPUT_HEAD_CHARS = 8000;
2376
+ var STREAMING_TOOL_INPUT_TAIL_CHARS = 16000;
2377
+ var STREAMING_TOOL_MESSAGE_THROTTLE_MS = 500;
2128
2378
  function useSessionStream(sessionId, enabled = true) {
2129
2379
  const queryClient = useQueryClient5();
2130
2380
  const clientRef = useRef(null);
2131
2381
  const assistantMessageIdRef = useRef(null);
2382
+ const toolInputBuffersRef = useRef(new Map);
2383
+ const toolPreviewEmitRef = useRef(new Map);
2384
+ const toolMessageEmitRef = useRef(new Map);
2132
2385
  const {
2133
2386
  addPendingApproval,
2134
2387
  removePendingApproval,
@@ -2140,6 +2393,9 @@ function useSessionStream(sessionId, enabled = true) {
2140
2393
  return;
2141
2394
  }
2142
2395
  assistantMessageIdRef.current = null;
2396
+ toolInputBuffersRef.current.clear();
2397
+ toolPreviewEmitRef.current.clear();
2398
+ toolMessageEmitRef.current.clear();
2143
2399
  let lastSessionInvalidation = 0;
2144
2400
  apiClient.getPendingApprovals(sessionId).then((result) => {
2145
2401
  if (result.ok && result.pending.length > 0) {
@@ -2163,7 +2419,7 @@ function useSessionStream(sessionId, enabled = true) {
2163
2419
  }
2164
2420
  for (let i = messages.length - 1;i >= 0; i -= 1) {
2165
2421
  const candidate = messages[i];
2166
- if (candidate.role === "assistant" && candidate.status !== "complete") {
2422
+ if (candidate.role === "assistant" && candidate.status === "pending") {
2167
2423
  return i;
2168
2424
  }
2169
2425
  }
@@ -2194,6 +2450,388 @@ function useSessionStream(sessionId, enabled = true) {
2194
2450
  return typeof payload?.toolName === "string" ? payload.toolName : null;
2195
2451
  };
2196
2452
  const getToolEventArgs = (payload) => payload?.args ?? payload?.input;
2453
+ const getToolBufferKey = (payload) => {
2454
+ const callId = getToolEventCallId(payload);
2455
+ if (callId)
2456
+ return callId;
2457
+ const name = getToolEventName(payload);
2458
+ return name ? `name:${name}` : null;
2459
+ };
2460
+ const parseArgsRecord = (value) => {
2461
+ if (value && typeof value === "object" && !Array.isArray(value)) {
2462
+ return value;
2463
+ }
2464
+ if (typeof value !== "string")
2465
+ return null;
2466
+ try {
2467
+ const parsed = JSON.parse(value);
2468
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
2469
+ } catch {
2470
+ return null;
2471
+ }
2472
+ };
2473
+ const normalizeLineNumber = (value) => {
2474
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value, 10) : Number.NaN;
2475
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : undefined;
2476
+ };
2477
+ const parseLineRange = (value) => {
2478
+ if (typeof value !== "string")
2479
+ return {};
2480
+ const match = value.match(/@(\d+)(?:-(\d+))?/);
2481
+ if (!match)
2482
+ return {};
2483
+ return {
2484
+ startLine: normalizeLineNumber(match[1]),
2485
+ endLine: normalizeLineNumber(match[2] ?? match[1])
2486
+ };
2487
+ };
2488
+ const getBoundedStreamingToolInput = (value) => {
2489
+ const maxLength = STREAMING_TOOL_INPUT_HEAD_CHARS + STREAMING_TOOL_INPUT_TAIL_CHARS;
2490
+ if (value.length <= maxLength)
2491
+ return value;
2492
+ return `${value.slice(0, STREAMING_TOOL_INPUT_HEAD_CHARS)}
2493
+ … streamed tool input truncated for UI responsiveness …
2494
+ ${value.slice(-STREAMING_TOOL_INPUT_TAIL_CHARS)}`;
2495
+ };
2496
+ const getToolArgsForViewer = (payload, delta) => {
2497
+ const args = parseArgsRecord(getToolEventArgs(payload));
2498
+ if (args)
2499
+ return args;
2500
+ const key = getToolBufferKey(payload);
2501
+ if (!key)
2502
+ return null;
2503
+ const previous = toolInputBuffersRef.current.get(key) ?? "";
2504
+ if (!delta)
2505
+ return parseArgsRecord(previous);
2506
+ const next = getBoundedStreamingToolInput(`${previous}${delta}`);
2507
+ toolInputBuffersRef.current.set(key, next);
2508
+ return parseArgsRecord(next);
2509
+ };
2510
+ const bestEffortUnescapeJsonString = (value) => {
2511
+ try {
2512
+ return JSON.parse(`"${value.replace(/\\$/, "")}"`);
2513
+ } catch {
2514
+ return value.replace(/\\n/g, `
2515
+ `).replace(/\\t/g, "\t").replace(/\\r/g, "\r").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
2516
+ }
2517
+ };
2518
+ const extractJsonStringField = (text, field, requireClosed = false) => {
2519
+ const marker = `"${field}"`;
2520
+ const markerIndex = text.indexOf(marker);
2521
+ if (markerIndex === -1)
2522
+ return;
2523
+ const colonIndex = text.indexOf(":", markerIndex + marker.length);
2524
+ if (colonIndex === -1)
2525
+ return;
2526
+ const quoteIndex = text.indexOf('"', colonIndex + 1);
2527
+ if (quoteIndex === -1)
2528
+ return;
2529
+ let escaped = "";
2530
+ let escaping = false;
2531
+ let closed = false;
2532
+ for (let i = quoteIndex + 1;i < text.length; i += 1) {
2533
+ const char = text[i];
2534
+ if (escaping) {
2535
+ escaped += `\\${char}`;
2536
+ escaping = false;
2537
+ continue;
2538
+ }
2539
+ if (char === "\\") {
2540
+ escaping = true;
2541
+ continue;
2542
+ }
2543
+ if (char === '"') {
2544
+ closed = true;
2545
+ break;
2546
+ }
2547
+ escaped += char;
2548
+ }
2549
+ if (requireClosed && !closed)
2550
+ return;
2551
+ return bestEffortUnescapeJsonString(escaped);
2552
+ };
2553
+ const getBufferedToolInput = (payload) => {
2554
+ const key = getToolBufferKey(payload);
2555
+ return key ? toolInputBuffersRef.current.get(key) ?? "" : "";
2556
+ };
2557
+ const getStringArg = (args, buffer, field, requireClosed = false) => {
2558
+ const value = args?.[field];
2559
+ if (typeof value === "string")
2560
+ return value;
2561
+ return extractJsonStringField(buffer, field, requireClosed);
2562
+ };
2563
+ const getStreamingWritePreviewContent = (args, buffer) => {
2564
+ const argContent = args?.content;
2565
+ if (typeof argContent === "string") {
2566
+ if (argContent.length <= STREAMING_WRITE_CONTENT_PREVIEW_CHARS) {
2567
+ return argContent;
2568
+ }
2569
+ return `… showing latest streamed content only …
2570
+ ${argContent.slice(-STREAMING_WRITE_CONTENT_PREVIEW_CHARS)}`;
2571
+ }
2572
+ const marker = '"content"';
2573
+ const markerIndex = buffer.indexOf(marker);
2574
+ if (markerIndex === -1)
2575
+ return;
2576
+ const colonIndex = buffer.indexOf(":", markerIndex + marker.length);
2577
+ if (colonIndex === -1)
2578
+ return;
2579
+ const quoteIndex = buffer.indexOf('"', colonIndex + 1);
2580
+ if (quoteIndex === -1)
2581
+ return;
2582
+ const valueStart = quoteIndex + 1;
2583
+ if (buffer.length - valueStart <= STREAMING_WRITE_CONTENT_PREVIEW_CHARS) {
2584
+ return extractJsonStringField(buffer, "content");
2585
+ }
2586
+ const rawTail = buffer.slice(Math.max(valueStart, buffer.length - STREAMING_WRITE_CONTENT_PREVIEW_CHARS));
2587
+ return `… showing latest streamed content only …
2588
+ ${bestEffortUnescapeJsonString(rawTail)}`;
2589
+ };
2590
+ const getStreamingPatchPreviewContent = (args, buffer) => {
2591
+ const argPatch = args?.patch;
2592
+ if (typeof argPatch === "string") {
2593
+ if (argPatch.length <= STREAMING_PATCH_PREVIEW_HEAD_CHARS + STREAMING_PATCH_PREVIEW_TAIL_CHARS) {
2594
+ return argPatch;
2595
+ }
2596
+ return `${argPatch.slice(0, STREAMING_PATCH_PREVIEW_HEAD_CHARS)}
2597
+ … patch preview truncated while streaming …
2598
+ ${argPatch.slice(-STREAMING_PATCH_PREVIEW_TAIL_CHARS)}`;
2599
+ }
2600
+ const marker = '"patch"';
2601
+ const markerIndex = buffer.indexOf(marker);
2602
+ if (markerIndex === -1)
2603
+ return;
2604
+ const colonIndex = buffer.indexOf(":", markerIndex + marker.length);
2605
+ if (colonIndex === -1)
2606
+ return;
2607
+ const quoteIndex = buffer.indexOf('"', colonIndex + 1);
2608
+ if (quoteIndex === -1)
2609
+ return;
2610
+ const valueStart = quoteIndex + 1;
2611
+ const rawLength = buffer.length - valueStart;
2612
+ if (rawLength <= STREAMING_PATCH_PREVIEW_HEAD_CHARS + STREAMING_PATCH_PREVIEW_TAIL_CHARS) {
2613
+ return extractJsonStringField(buffer, "patch");
2614
+ }
2615
+ const rawHead = buffer.slice(valueStart, valueStart + STREAMING_PATCH_PREVIEW_HEAD_CHARS);
2616
+ const rawTail = buffer.slice(-STREAMING_PATCH_PREVIEW_TAIL_CHARS);
2617
+ return `${bestEffortUnescapeJsonString(rawHead)}
2618
+ … patch preview truncated while streaming …
2619
+ ${bestEffortUnescapeJsonString(rawTail)}`;
2620
+ };
2621
+ const getResultRecord = (payload) => payload?.result && typeof payload.result === "object" && !Array.isArray(payload.result) ? payload.result : null;
2622
+ const getArtifactRecord = (payload) => payload?.artifact && typeof payload.artifact === "object" && !Array.isArray(payload.artifact) ? payload.artifact : null;
2623
+ const extractErrorMessage2 = (payload) => {
2624
+ const result = getResultRecord(payload);
2625
+ if (typeof payload?.error === "string")
2626
+ return payload.error;
2627
+ return typeof result?.error === "string" ? result.error : undefined;
2628
+ };
2629
+ const normalizePatchPath = (path) => path.replace(/^a\//, "").replace(/^b\//, "").trim();
2630
+ const patchPathMatches = (patchPath, targetPath) => {
2631
+ const normalizedPatch = normalizePatchPath(patchPath);
2632
+ const normalizedTarget = normalizePatchPath(targetPath);
2633
+ return normalizedPatch === normalizedTarget || normalizedPatch.endsWith(`/${normalizedTarget}`) || normalizedTarget.endsWith(`/${normalizedPatch}`);
2634
+ };
2635
+ const patchPathMayReferToTarget = (patchPath, targetPath) => {
2636
+ const normalizedPatch = normalizePatchPath(patchPath).replace(/\/+$/, "");
2637
+ const normalizedTarget = normalizePatchPath(targetPath);
2638
+ if (!normalizedPatch)
2639
+ return false;
2640
+ return patchPathMatches(patchPath, targetPath) || normalizedTarget.startsWith(`${normalizedPatch}/`);
2641
+ };
2642
+ const isLikelyCompletePatchPath = (path) => {
2643
+ const normalized = normalizePatchPath(path);
2644
+ if (!normalized || normalized.endsWith("/"))
2645
+ return false;
2646
+ const name = normalized.split("/").pop() ?? "";
2647
+ return name.includes(".");
2648
+ };
2649
+ const getCompletedPatchChangeLineSignature = (patch) => {
2650
+ const stablePatch = patch.endsWith(`
2651
+ `) ? patch : patch.slice(0, patch.lastIndexOf(`
2652
+ `) + 1);
2653
+ if (!stablePatch)
2654
+ return;
2655
+ let changeLines = 0;
2656
+ let stableChangeLength = 0;
2657
+ for (const line of stablePatch.split(`
2658
+ `)) {
2659
+ if (line.startsWith("+") && !line.startsWith("+++") || line.startsWith("-") && !line.startsWith("---")) {
2660
+ changeLines += 1;
2661
+ stableChangeLength += line.length;
2662
+ }
2663
+ }
2664
+ return changeLines > 0 ? `${changeLines}:${stableChangeLength}` : undefined;
2665
+ };
2666
+ const extractPathsFromPatch = (patch) => {
2667
+ const paths = new Set;
2668
+ for (const line of patch.split(`
2669
+ `)) {
2670
+ const directive = line.match(/^\*\*\* (?:Update|Add|Delete) File: (.+)$/);
2671
+ if (directive?.[1]) {
2672
+ paths.add(directive[1].trim());
2673
+ continue;
2674
+ }
2675
+ const unified = line.match(/^\+\+\+ (?:b\/)?(.+)$/);
2676
+ if (unified?.[1] && unified[1] !== "/dev/null") {
2677
+ paths.add(unified[1].trim());
2678
+ }
2679
+ }
2680
+ return [...paths];
2681
+ };
2682
+ const getChangedLinesForPath = (result, path) => {
2683
+ const changes = Array.isArray(result?.changes) ? result.changes : [];
2684
+ const lines = new Set;
2685
+ for (const change of changes) {
2686
+ if (!change || typeof change !== "object")
2687
+ continue;
2688
+ const record = change;
2689
+ if (typeof record.filePath !== "string")
2690
+ continue;
2691
+ if (!patchPathMatches(record.filePath, path))
2692
+ continue;
2693
+ if (!Array.isArray(record.hunks))
2694
+ continue;
2695
+ for (const hunk of record.hunks) {
2696
+ if (!hunk || typeof hunk !== "object")
2697
+ continue;
2698
+ const hunkRecord = hunk;
2699
+ const newStart = typeof hunkRecord.newStart === "number" ? hunkRecord.newStart : undefined;
2700
+ const newLines = typeof hunkRecord.newLines === "number" ? hunkRecord.newLines : undefined;
2701
+ if (!newStart || !newLines)
2702
+ continue;
2703
+ for (let line = newStart;line < newStart + newLines; line += 1) {
2704
+ lines.add(line);
2705
+ }
2706
+ }
2707
+ }
2708
+ return lines.size > 0 ? [...lines] : undefined;
2709
+ };
2710
+ const handleReadToolActivity = (eventType, payload, delta) => {
2711
+ const viewerStore = useViewerTabsStore.getState();
2712
+ if (!viewerStore.followToolActivity)
2713
+ return;
2714
+ const name = getToolEventName(payload);
2715
+ if (name !== "read")
2716
+ return;
2717
+ const args = getToolArgsForViewer(payload, delta);
2718
+ const path = typeof args?.path === "string" ? args.path : null;
2719
+ if (!path)
2720
+ return;
2721
+ const result = payload?.result && typeof payload.result === "object" && !Array.isArray(payload.result) ? payload.result : null;
2722
+ const rangeFromResult = parseLineRange(result?.lineRange);
2723
+ const startLine = normalizeLineNumber(args.startLine) ?? normalizeLineNumber(args.start_line) ?? rangeFromResult.startLine;
2724
+ const endLine = normalizeLineNumber(args.endLine) ?? normalizeLineNumber(args.end_line) ?? rangeFromResult.endLine ?? startLine;
2725
+ const failed = result?.ok === false || eventType === "error";
2726
+ viewerStore.openToolReadTab(path, {
2727
+ startLine,
2728
+ endLine,
2729
+ reason: "read",
2730
+ callId: getToolEventCallId(payload) ?? undefined,
2731
+ status: failed ? "error" : eventType === "tool.result" ? "success" : "streaming"
2732
+ });
2733
+ };
2734
+ const handleWriteToolActivity = (eventType, payload, delta) => {
2735
+ const viewerStore = useViewerTabsStore.getState();
2736
+ if (!viewerStore.followToolActivity)
2737
+ return;
2738
+ const name = getToolEventName(payload);
2739
+ if (name !== "write")
2740
+ return;
2741
+ const args = getToolArgsForViewer(payload, delta);
2742
+ const buffer = getBufferedToolInput(payload);
2743
+ const result = getResultRecord(payload);
2744
+ const path = (typeof result?.path === "string" ? result.path : undefined) ?? getStringArg(args, buffer, "path", true);
2745
+ if (!path)
2746
+ return;
2747
+ const failed = result?.ok === false || eventType === "error";
2748
+ const callId = getToolEventCallId(payload) ?? undefined;
2749
+ const status = failed ? "error" : eventType === "tool.result" ? "success" : "streaming";
2750
+ const content = status === "streaming" ? getStreamingWritePreviewContent(args, buffer) : getStringArg(args, buffer, "content");
2751
+ if (status === "streaming" && content !== undefined && content.length >= TOOL_PREVIEW_THROTTLE_MIN_CHARS) {
2752
+ const previewKey = callId ?? path;
2753
+ const now = Date.now();
2754
+ const last = toolPreviewEmitRef.current.get(previewKey);
2755
+ const contentDelta = Math.abs(content.length - (last?.contentLength ?? 0));
2756
+ if (last && now - last.emittedAt < TOOL_PREVIEW_THROTTLE_MS && contentDelta < TOOL_PREVIEW_THROTTLE_MIN_DELTA_CHARS) {
2757
+ return;
2758
+ }
2759
+ toolPreviewEmitRef.current.set(previewKey, {
2760
+ emittedAt: now,
2761
+ contentLength: content.length
2762
+ });
2763
+ }
2764
+ viewerStore.openToolPreviewTab({
2765
+ path,
2766
+ toolName: "write",
2767
+ callId,
2768
+ content,
2769
+ status,
2770
+ error: extractErrorMessage2(payload)
2771
+ });
2772
+ };
2773
+ const handleApplyPatchToolActivity = (eventType, payload, delta) => {
2774
+ const viewerStore = useViewerTabsStore.getState();
2775
+ if (!viewerStore.followToolActivity)
2776
+ return;
2777
+ const name = getToolEventName(payload);
2778
+ if (name !== "apply_patch")
2779
+ return;
2780
+ const args = getToolArgsForViewer(payload, delta);
2781
+ const buffer = getBufferedToolInput(payload);
2782
+ const artifact = getArtifactRecord(payload);
2783
+ const result = getResultRecord(payload);
2784
+ const failed = result?.ok === false || eventType === "error";
2785
+ const status = failed ? "error" : eventType === "tool.result" ? "success" : "streaming";
2786
+ const callId = getToolEventCallId(payload) ?? undefined;
2787
+ const patch = (typeof artifact?.patch === "string" ? artifact.patch : undefined) ?? (status === "streaming" ? getStreamingPatchPreviewContent(args, buffer) : getStringArg(args, buffer, "patch"));
2788
+ if (!patch)
2789
+ return;
2790
+ const previewKey = callId ?? "apply_patch";
2791
+ const lineSignature = status === "streaming" ? getCompletedPatchChangeLineSignature(patch) : undefined;
2792
+ if (status === "streaming") {
2793
+ const last = toolPreviewEmitRef.current.get(previewKey);
2794
+ if (last?.lineSignature === lineSignature)
2795
+ return;
2796
+ }
2797
+ const patchPaths = extractPathsFromPatch(patch);
2798
+ if (patchPaths.length === 0)
2799
+ return;
2800
+ const matchingFileTabs = viewerStore.tabs.filter((tab) => tab.type === "file" && patchPaths.some((path) => patchPathMayReferToTarget(path, tab.path)));
2801
+ const activeMatchingFileTab = matchingFileTabs.find((tab) => tab.id === viewerStore.activeTabId);
2802
+ const fallbackPath = patchPaths.find(isLikelyCompletePatchPath);
2803
+ if (!activeMatchingFileTab && !matchingFileTabs[0] && !fallbackPath)
2804
+ return;
2805
+ const targetPath = activeMatchingFileTab?.path ?? matchingFileTabs[0]?.path ?? fallbackPath;
2806
+ if (!targetPath)
2807
+ return;
2808
+ viewerStore.openToolPreviewTab({
2809
+ path: targetPath,
2810
+ toolName: "apply_patch",
2811
+ callId,
2812
+ patch,
2813
+ changedLines: getChangedLinesForPath(result, targetPath),
2814
+ status,
2815
+ error: extractErrorMessage2(payload)
2816
+ });
2817
+ if (status === "streaming") {
2818
+ toolPreviewEmitRef.current.set(previewKey, {
2819
+ emittedAt: Date.now(),
2820
+ contentLength: buffer.length,
2821
+ lineSignature
2822
+ });
2823
+ }
2824
+ };
2825
+ const handleToolActivityViewerEvent = (eventType, payload, delta) => {
2826
+ const name = getToolEventName(payload);
2827
+ if (name === "read")
2828
+ handleReadToolActivity(eventType, payload, delta);
2829
+ if (name === "write")
2830
+ handleWriteToolActivity(eventType, payload, delta);
2831
+ if (name === "apply_patch") {
2832
+ handleApplyPatchToolActivity(eventType, payload, delta);
2833
+ }
2834
+ };
2197
2835
  const getToolInputDelta = (payload) => {
2198
2836
  if (typeof payload?.delta === "string")
2199
2837
  return payload.delta;
@@ -2204,19 +2842,9 @@ function useSessionStream(sessionId, enabled = true) {
2204
2842
  return payload.delta;
2205
2843
  return typeof payload?.outputTextDelta === "string" ? payload.outputTextDelta : null;
2206
2844
  };
2207
- const getOptimisticPartIndex = (parts, stepIndex) => {
2208
- const appendIndex = (() => {
2209
- const indexes = parts.map((part) => part.index).filter((index) => Number.isFinite(index));
2210
- return indexes.length > 0 ? Math.max(...indexes) + 0.001 : 0;
2211
- })();
2212
- if (typeof stepIndex !== "number") {
2213
- return appendIndex;
2214
- }
2215
- const sameStepIndexes = parts.filter((part) => part.stepIndex === stepIndex).map((part) => part.index).filter((index) => Number.isFinite(index));
2216
- if (sameStepIndexes.length > 0) {
2217
- return Math.max(...sameStepIndexes) + 0.001;
2218
- }
2219
- return appendIndex;
2845
+ const getOptimisticPartIndex = (parts, _stepIndex) => {
2846
+ const indexes = parts.map((part) => part.index).filter((index) => Number.isFinite(index));
2847
+ return indexes.length > 0 ? Math.max(...indexes) + 0.001 : 0;
2220
2848
  };
2221
2849
  const applyReasoningDelta = (payload) => {
2222
2850
  const messageId = typeof payload?.messageId === "string" ? payload.messageId : null;
@@ -2331,6 +2959,18 @@ function useSessionStream(sessionId, enabled = true) {
2331
2959
  const name = getToolEventName(payload);
2332
2960
  if (!name)
2333
2961
  return;
2962
+ if (name === "write" || name === "apply_patch") {
2963
+ const bufferKey = getToolBufferKey(payload);
2964
+ const bufferedLength = bufferKey ? toolInputBuffersRef.current.get(bufferKey)?.length ?? 0 : 0;
2965
+ if (bufferedLength >= TOOL_PREVIEW_THROTTLE_MIN_CHARS) {
2966
+ const emitKey = callId ?? `name:${name}`;
2967
+ const now = Date.now();
2968
+ const last = toolMessageEmitRef.current.get(emitKey) ?? 0;
2969
+ if (now - last < STREAMING_TOOL_MESSAGE_THROTTLE_MS)
2970
+ return;
2971
+ toolMessageEmitRef.current.set(emitKey, now);
2972
+ }
2973
+ }
2334
2974
  queryClient.setQueryData(["messages", sessionId], (oldMessages) => {
2335
2975
  if (!oldMessages)
2336
2976
  return oldMessages;
@@ -2434,7 +3074,7 @@ function useSessionStream(sessionId, enabled = true) {
2434
3074
  if (partIndex === -1) {
2435
3075
  const contentJsonBase = {
2436
3076
  name,
2437
- _streamedInput: delta
3077
+ _streamedInput: getBoundedStreamingToolInput(delta)
2438
3078
  };
2439
3079
  if (callId)
2440
3080
  contentJsonBase.callId = callId;
@@ -2462,7 +3102,7 @@ function useSessionStream(sessionId, enabled = true) {
2462
3102
  const prev = typeof existing.contentJson?._streamedInput === "string" ? existing.contentJson._streamedInput : "";
2463
3103
  const nextContentJson = {
2464
3104
  ...typeof existing.contentJson === "object" && !Array.isArray(existing.contentJson) ? existing.contentJson : {},
2465
- _streamedInput: prev + delta
3105
+ _streamedInput: getBoundedStreamingToolInput(prev + delta)
2466
3106
  };
2467
3107
  parts[partIndex] = {
2468
3108
  ...existing,
@@ -2671,6 +3311,7 @@ function useSessionStream(sessionId, enabled = true) {
2671
3311
  const id = typeof payload?.id === "string" ? payload.id : null;
2672
3312
  if (role === "assistant" && id) {
2673
3313
  assistantMessageIdRef.current = id;
3314
+ optimisticallyQueueMessage(queryClient, sessionId, id);
2674
3315
  }
2675
3316
  if (id && role) {
2676
3317
  const agent = typeof payload?.agent === "string" ? payload.agent : "";
@@ -2750,8 +3391,10 @@ function useSessionStream(sessionId, enabled = true) {
2750
3391
  if (channel === "input" || channel == null && delta) {
2751
3392
  if (delta) {
2752
3393
  accumulateToolInputDelta(payload, delta);
3394
+ handleToolActivityViewerEvent("tool.delta", payload, delta);
2753
3395
  } else {
2754
3396
  upsertEphemeralToolCall(payload);
3397
+ handleToolActivityViewerEvent("tool.delta", payload);
2755
3398
  }
2756
3399
  } else if (channel === "output" && delta) {
2757
3400
  accumulateToolOutputDelta(payload, delta);
@@ -2760,10 +3403,15 @@ function useSessionStream(sessionId, enabled = true) {
2760
3403
  }
2761
3404
  case "tool.call": {
2762
3405
  upsertEphemeralToolCall(payload);
3406
+ handleToolActivityViewerEvent("tool.call", payload);
2763
3407
  break;
2764
3408
  }
2765
3409
  case "tool.result": {
2766
3410
  resolveEphemeralToolCall(payload);
3411
+ handleToolActivityViewerEvent("tool.result", payload);
3412
+ const key = getToolBufferKey(payload);
3413
+ if (key)
3414
+ toolInputBuffersRef.current.delete(key);
2767
3415
  break;
2768
3416
  }
2769
3417
  case "tool.approval.required": {
@@ -2798,10 +3446,30 @@ function useSessionStream(sessionId, enabled = true) {
2798
3446
  break;
2799
3447
  }
2800
3448
  case "error": {
3449
+ handleToolActivityViewerEvent("error", payload);
2801
3450
  removeEphemeralToolCall(payload);
2802
3451
  const messageId = typeof payload?.messageId === "string" ? payload.messageId : null;
2803
3452
  if (messageId) {
3453
+ if (assistantMessageIdRef.current === messageId) {
3454
+ assistantMessageIdRef.current = null;
3455
+ }
2804
3456
  clearEphemeralForMessage(messageId);
3457
+ const errorMessage = typeof payload?.error === "string" ? payload.error : typeof payload?.message === "string" ? payload.message : "Assistant run failed";
3458
+ queryClient.setQueryData(["messages", sessionId], (oldMessages) => {
3459
+ if (!oldMessages)
3460
+ return oldMessages;
3461
+ const idx = oldMessages.findIndex((m) => m.id === messageId);
3462
+ if (idx === -1)
3463
+ return oldMessages;
3464
+ const next = [...oldMessages];
3465
+ next[idx] = {
3466
+ ...next[idx],
3467
+ status: "error",
3468
+ completedAt: next[idx].completedAt ?? Date.now(),
3469
+ error: errorMessage
3470
+ };
3471
+ return next;
3472
+ });
2805
3473
  }
2806
3474
  queryClient.invalidateQueries({ queryKey: ["messages", sessionId] });
2807
3475
  break;
@@ -2810,6 +3478,13 @@ function useSessionStream(sessionId, enabled = true) {
2810
3478
  const id = typeof payload?.id === "string" ? payload.id : null;
2811
3479
  const status = typeof payload?.status === "string" ? payload.status : null;
2812
3480
  if (id && status) {
3481
+ if (status !== "pending" && assistantMessageIdRef.current === id) {
3482
+ assistantMessageIdRef.current = null;
3483
+ }
3484
+ if (status !== "pending") {
3485
+ clearEphemeralForMessage(id);
3486
+ }
3487
+ const error = typeof payload?.error === "string" ? payload.error : undefined;
2813
3488
  queryClient.setQueryData(["messages", sessionId], (oldMessages) => {
2814
3489
  if (!oldMessages)
2815
3490
  return oldMessages;
@@ -2819,7 +3494,9 @@ function useSessionStream(sessionId, enabled = true) {
2819
3494
  const next = [...oldMessages];
2820
3495
  next[idx] = {
2821
3496
  ...next[idx],
2822
- status
3497
+ status,
3498
+ completedAt: status === "pending" ? next[idx].completedAt : next[idx].completedAt ?? Date.now(),
3499
+ error: error ?? next[idx].error
2823
3500
  };
2824
3501
  return next;
2825
3502
  });
@@ -3022,6 +3699,9 @@ function sendBrowserNotification(notification) {
3022
3699
  };
3023
3700
  return true;
3024
3701
  }
3702
+ function isAppForeground() {
3703
+ return getPlatformWindowFocused() ?? document.visibilityState === "visible";
3704
+ }
3025
3705
  function updateSessionStatusInCache(queryClient, status) {
3026
3706
  queryClient.setQueryData(sessionsQueryKey, (old) => {
3027
3707
  if (!old)
@@ -3167,7 +3847,7 @@ function useClientEvents(activeSessionId) {
3167
3847
  }
3168
3848
  if (event.event === "notification") {
3169
3849
  const notification = payload;
3170
- const isActiveVisibleSession = notification.sessionId === activeSessionIdRef.current && document.visibilityState === "visible";
3850
+ const isActiveVisibleSession = notification.sessionId === activeSessionIdRef.current && isAppForeground();
3171
3851
  const isSessionNotification = notification.source === "session" || !!notification.sessionId;
3172
3852
  let sentSystemNotification = false;
3173
3853
  if (!isActiveVisibleSession) {
@@ -3423,7 +4103,7 @@ function useKeyboardShortcuts({
3423
4103
  }
3424
4104
  return;
3425
4105
  }
3426
- if ((e.ctrlKey || e.metaKey) && e.key === "h") {
4106
+ if ((e.ctrlKey || e.metaKey) && e.key === "b") {
3427
4107
  e.preventDefault();
3428
4108
  if (currentFocus === "sessions") {
3429
4109
  document.activeElement?.blur();
@@ -3446,7 +4126,7 @@ function useKeyboardShortcuts({
3446
4126
  }
3447
4127
  return;
3448
4128
  }
3449
- if ((e.ctrlKey || e.metaKey) && e.key === "l") {
4129
+ if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key === "r") {
3450
4130
  e.preventDefault();
3451
4131
  if (currentFocus === "git") {
3452
4132
  document.activeElement?.blur();
@@ -4162,10 +4842,10 @@ function useFileUpload(options = {}) {
4162
4842
  };
4163
4843
  }
4164
4844
  // src/hooks/useSessionFiles.ts
4165
- import { useQuery as useQuery5 } from "@tanstack/react-query";
4845
+ import { useQuery as useQuery6 } from "@tanstack/react-query";
4166
4846
  function useSessionFiles(sessionId, enabled = true) {
4167
4847
  const isExpanded = useSessionFilesStore((state) => state.isExpanded);
4168
- return useQuery5({
4848
+ return useQuery6({
4169
4849
  queryKey: ["session", sessionId, "files"],
4170
4850
  queryFn: () => sessionId ? apiClient.getSessionFiles(sessionId) : null,
4171
4851
  enabled: !!sessionId && enabled,
@@ -4174,46 +4854,6 @@ function useSessionFiles(sessionId, enabled = true) {
4174
4854
  staleTime: 3000
4175
4855
  });
4176
4856
  }
4177
- // src/hooks/useQueueState.ts
4178
- import { useQuery as useQuery6 } from "@tanstack/react-query";
4179
- var defaultQueueState = {
4180
- currentMessageId: null,
4181
- queuedMessages: [],
4182
- queueLength: 0
4183
- };
4184
- function useQueueState(sessionId) {
4185
- const { data } = useQuery6({
4186
- queryKey: ["queueState", sessionId],
4187
- queryFn: async () => {
4188
- if (!sessionId)
4189
- return defaultQueueState;
4190
- const queueState = await apiClient.getQueueState(sessionId);
4191
- return {
4192
- currentMessageId: queueState.currentMessageId,
4193
- queuedMessages: queueState.queuedMessages,
4194
- queueLength: queueState.queuedMessages.length
4195
- };
4196
- },
4197
- enabled: !!sessionId,
4198
- placeholderData: defaultQueueState,
4199
- staleTime: Infinity
4200
- });
4201
- return data ?? defaultQueueState;
4202
- }
4203
- function useMessageQueuePosition(sessionId, messageId) {
4204
- const queueState = useQueueState(sessionId);
4205
- if (!sessionId || !queueState) {
4206
- return { isQueued: false, isRunning: false, position: null };
4207
- }
4208
- if (queueState.currentMessageId === messageId) {
4209
- return { isQueued: false, isRunning: true, position: null };
4210
- }
4211
- const queuedItem = queueState.queuedMessages.find((item) => item.messageId === messageId);
4212
- if (queuedItem) {
4213
- return { isQueued: true, isRunning: false, position: queuedItem.position };
4214
- }
4215
- return { isQueued: false, isRunning: false, position: null };
4216
- }
4217
4857
  // src/hooks/useBranch.ts
4218
4858
  import { useQuery as useQuery7, useMutation as useMutation5, useQueryClient as useQueryClient7 } from "@tanstack/react-query";
4219
4859
  function useCreateBranch(sessionId) {
@@ -5767,6 +6407,7 @@ export {
5767
6407
  useImageUpload,
5768
6408
  useGitStatus,
5769
6409
  useGitRemotes,
6410
+ useGitRebaseAction,
5770
6411
  useGitInit,
5771
6412
  useGitDiffFullFile,
5772
6413
  useGitDiff,
@@ -5794,7 +6435,8 @@ export {
5794
6435
  useAllModels,
5795
6436
  useAddRemote,
5796
6437
  useAddMCPServer,
5797
- sessionsQueryKey
6438
+ sessionsQueryKey,
6439
+ optimisticallyQueueMessage
5798
6440
  };
5799
6441
 
5800
- //# debugId=AFCFC55375F9489664756E2164756E21
6442
+ //# debugId=90296322954CBDC864756E2164756E21