@grackle-ai/web-components 0.107.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.rush/temp/3ae72563f781afd72723475938136f113846603e.untar.log +10 -0
- package/.rush/temp/bc1d5bf9201ce71abeaeaddd096deb9b0805d703.untar.log +10 -0
- package/.rush/temp/operation/_phase_build/all.log +18 -0
- package/.rush/temp/operation/_phase_build/log-chunks.jsonl +18 -0
- package/.rush/temp/operation/_phase_build/state.json +3 -0
- package/.rush/temp/operation/_phase_test/all.log +121 -0
- package/.rush/temp/operation/_phase_test/log-chunks.jsonl +121 -0
- package/.rush/temp/operation/_phase_test/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +938 -0
- package/.storybook/main.ts +22 -0
- package/.storybook/preview.tsx +30 -0
- package/config/rig.json +4 -0
- package/config/rush-project.json +12 -0
- package/dist/index.css +1 -0
- package/dist/index.js +39221 -0
- package/eslint.config.cjs +5 -0
- package/package.json +83 -0
- package/rush-logs/web-components._phase_build.cache.log +4 -0
- package/rush-logs/web-components._phase_test.cache.log +4 -0
- package/src/components/chat/ChatInput.module.scss +81 -0
- package/src/components/chat/ChatInput.stories.tsx +91 -0
- package/src/components/chat/ChatInput.tsx +168 -0
- package/src/components/chat/index.ts +6 -0
- package/src/components/dag/DagView.module.scss +149 -0
- package/src/components/dag/DagView.stories.tsx +125 -0
- package/src/components/dag/DagView.tsx +109 -0
- package/src/components/dag/TaskNode.stories.tsx +133 -0
- package/src/components/dag/TaskNode.tsx +40 -0
- package/src/components/dag/useDagLayout.ts +139 -0
- package/src/components/display/Breadcrumbs.module.scss +71 -0
- package/src/components/display/Breadcrumbs.stories.tsx +80 -0
- package/src/components/display/Breadcrumbs.tsx +46 -0
- package/src/components/display/Button.module.scss +110 -0
- package/src/components/display/Button.stories.tsx +88 -0
- package/src/components/display/Button.tsx +40 -0
- package/src/components/display/ConfirmDialog.module.scss +67 -0
- package/src/components/display/ConfirmDialog.stories.tsx +81 -0
- package/src/components/display/ConfirmDialog.tsx +88 -0
- package/src/components/display/CopyButton.module.scss +41 -0
- package/src/components/display/CopyButton.stories.tsx +78 -0
- package/src/components/display/CopyButton.tsx +64 -0
- package/src/components/display/DemoBanner.module.scss +37 -0
- package/src/components/display/DemoBanner.stories.tsx +40 -0
- package/src/components/display/DemoBanner.tsx +23 -0
- package/src/components/display/EventHoverRow.module.scss +102 -0
- package/src/components/display/EventHoverRow.stories.tsx +99 -0
- package/src/components/display/EventHoverRow.tsx +154 -0
- package/src/components/display/EventRenderer.module.scss +272 -0
- package/src/components/display/EventRenderer.stories.tsx +186 -0
- package/src/components/display/EventRenderer.tsx +271 -0
- package/src/components/display/EventStream.module.scss +93 -0
- package/src/components/display/EventStream.stories.tsx +249 -0
- package/src/components/display/EventStream.tsx +369 -0
- package/src/components/display/FloatingActionBar.module.scss +107 -0
- package/src/components/display/FloatingActionBar.stories.tsx +122 -0
- package/src/components/display/FloatingActionBar.tsx +119 -0
- package/src/components/display/SessionAttemptSelector.module.scss +50 -0
- package/src/components/display/SessionAttemptSelector.stories.tsx +78 -0
- package/src/components/display/SessionAttemptSelector.tsx +49 -0
- package/src/components/display/SessionPicker.module.scss +200 -0
- package/src/components/display/SessionPicker.stories.tsx +169 -0
- package/src/components/display/SessionPicker.tsx +214 -0
- package/src/components/display/Skeleton.module.scss +58 -0
- package/src/components/display/Skeleton.stories.tsx +94 -0
- package/src/components/display/Skeleton.tsx +127 -0
- package/src/components/display/Spinner.module.scss +41 -0
- package/src/components/display/Spinner.stories.tsx +66 -0
- package/src/components/display/Spinner.tsx +32 -0
- package/src/components/display/SplashScreen.module.scss +20 -0
- package/src/components/display/SplashScreen.stories.tsx +26 -0
- package/src/components/display/SplashScreen.tsx +16 -0
- package/src/components/display/SplitButton.module.scss +166 -0
- package/src/components/display/SplitButton.stories.tsx +95 -0
- package/src/components/display/SplitButton.tsx +128 -0
- package/src/components/display/Tooltip.module.scss +84 -0
- package/src/components/display/Tooltip.stories.tsx +240 -0
- package/src/components/display/Tooltip.tsx +184 -0
- package/src/components/display/extractText.test.tsx +48 -0
- package/src/components/display/index.ts +20 -0
- package/src/components/editable/EditableCheckbox.stories.tsx +54 -0
- package/src/components/editable/EditableCheckbox.tsx +39 -0
- package/src/components/editable/EditableField.module.scss +135 -0
- package/src/components/editable/EditableSelect.tsx +164 -0
- package/src/components/editable/EditableTextArea.stories.tsx +50 -0
- package/src/components/editable/EditableTextArea.tsx +148 -0
- package/src/components/editable/EditableTextField.stories.tsx +62 -0
- package/src/components/editable/EditableTextField.tsx +153 -0
- package/src/components/editable/EnvironmentSelect.module.scss +17 -0
- package/src/components/editable/EnvironmentSelect.stories.tsx +61 -0
- package/src/components/editable/EnvironmentSelect.tsx +87 -0
- package/src/components/editable/index.ts +13 -0
- package/src/components/editable/useEditableField.test.tsx +233 -0
- package/src/components/editable/useEditableField.ts +173 -0
- package/src/components/index.ts +20 -0
- package/src/components/knowledge/KnowledgeDetailPanel.module.scss +162 -0
- package/src/components/knowledge/KnowledgeDetailPanel.stories.tsx +208 -0
- package/src/components/knowledge/KnowledgeDetailPanel.tsx +122 -0
- package/src/components/knowledge/KnowledgeGraph.module.scss +110 -0
- package/src/components/knowledge/KnowledgeGraph.stories.tsx +180 -0
- package/src/components/knowledge/KnowledgeGraph.tsx +455 -0
- package/src/components/knowledge/KnowledgeNav.module.scss +130 -0
- package/src/components/knowledge/KnowledgeNav.stories.tsx +108 -0
- package/src/components/knowledge/KnowledgeNav.tsx +138 -0
- package/src/components/knowledge/index.ts +3 -0
- package/src/components/layout/AppNav.module.scss +82 -0
- package/src/components/layout/AppNav.stories.tsx +115 -0
- package/src/components/layout/AppNav.tsx +133 -0
- package/src/components/layout/BottomStatusBar.module.scss +58 -0
- package/src/components/layout/BottomStatusBar.stories.tsx +35 -0
- package/src/components/layout/BottomStatusBar.tsx +206 -0
- package/src/components/layout/Sidebar.module.scss +60 -0
- package/src/components/layout/Sidebar.stories.tsx +46 -0
- package/src/components/layout/Sidebar.tsx +84 -0
- package/src/components/layout/StatusBar.module.scss +108 -0
- package/src/components/layout/StatusBar.stories.tsx +119 -0
- package/src/components/layout/StatusBar.tsx +70 -0
- package/src/components/layout/index.ts +9 -0
- package/src/components/lists/EnvironmentNav.module.scss +118 -0
- package/src/components/lists/EnvironmentNav.stories.tsx +121 -0
- package/src/components/lists/EnvironmentNav.tsx +133 -0
- package/src/components/lists/FindingsNav.module.scss +126 -0
- package/src/components/lists/FindingsNav.tsx +146 -0
- package/src/components/lists/TaskList.module.scss +206 -0
- package/src/components/lists/TaskList.stories.tsx +401 -0
- package/src/components/lists/TaskList.tsx +509 -0
- package/src/components/lists/index.ts +6 -0
- package/src/components/lists/listHelpers.tsx +130 -0
- package/src/components/notifications/Callout.module.scss +83 -0
- package/src/components/notifications/Callout.stories.tsx +81 -0
- package/src/components/notifications/Callout.tsx +64 -0
- package/src/components/notifications/Toast.module.scss +86 -0
- package/src/components/notifications/Toast.stories.tsx +71 -0
- package/src/components/notifications/Toast.tsx +51 -0
- package/src/components/notifications/ToastContainer.module.scss +23 -0
- package/src/components/notifications/ToastContainer.stories.tsx +66 -0
- package/src/components/notifications/ToastContainer.tsx +29 -0
- package/src/components/notifications/UpdateBanner.stories.tsx +77 -0
- package/src/components/notifications/UpdateBanner.test.tsx +64 -0
- package/src/components/notifications/UpdateBanner.tsx +44 -0
- package/src/components/notifications/index.ts +8 -0
- package/src/components/panels/AboutPanel.stories.tsx +70 -0
- package/src/components/panels/AboutPanel.tsx +66 -0
- package/src/components/panels/AppearancePanel.stories.tsx +45 -0
- package/src/components/panels/AppearancePanel.tsx +97 -0
- package/src/components/panels/CredentialProvidersPanel.stories.tsx +62 -0
- package/src/components/panels/CredentialProvidersPanel.tsx +111 -0
- package/src/components/panels/EnvironmentEditPanel.module.scss +170 -0
- package/src/components/panels/EnvironmentEditPanel.stories.tsx +206 -0
- package/src/components/panels/EnvironmentEditPanel.tsx +785 -0
- package/src/components/panels/FindingsPanel.module.scss +94 -0
- package/src/components/panels/FindingsPanel.stories.tsx +109 -0
- package/src/components/panels/FindingsPanel.tsx +76 -0
- package/src/components/panels/KeyboardShortcutsPanel.module.scss +65 -0
- package/src/components/panels/KeyboardShortcutsPanel.stories.tsx +40 -0
- package/src/components/panels/KeyboardShortcutsPanel.tsx +104 -0
- package/src/components/panels/PluginsPanel.tsx +77 -0
- package/src/components/panels/SettingsPanel.module.scss +336 -0
- package/src/components/panels/TaskActionButtons.module.scss +22 -0
- package/src/components/panels/TaskActionButtons.stories.tsx +125 -0
- package/src/components/panels/TaskActionButtons.tsx +87 -0
- package/src/components/panels/TaskEditPanel.module.scss +202 -0
- package/src/components/panels/TaskEditPanel.stories.tsx +75 -0
- package/src/components/panels/TaskEditPanel.tsx +328 -0
- package/src/components/panels/TaskOverviewPanel.module.scss +236 -0
- package/src/components/panels/TaskOverviewPanel.stories.tsx +219 -0
- package/src/components/panels/TaskOverviewPanel.tsx +270 -0
- package/src/components/panels/TokensPanel.stories.tsx +131 -0
- package/src/components/panels/TokensPanel.tsx +143 -0
- package/src/components/panels/WorkpadPanel.module.scss +39 -0
- package/src/components/panels/WorkpadPanel.stories.tsx +56 -0
- package/src/components/panels/WorkpadPanel.tsx +63 -0
- package/src/components/panels/index.ts +13 -0
- package/src/components/personas/McpToolSelector.module.scss +109 -0
- package/src/components/personas/McpToolSelector.stories.tsx +129 -0
- package/src/components/personas/McpToolSelector.tsx +180 -0
- package/src/components/personas/PersonaManager.module.scss +233 -0
- package/src/components/personas/PersonaManager.stories.tsx +139 -0
- package/src/components/personas/PersonaManager.tsx +122 -0
- package/src/components/schedules/ScheduleManager.module.scss +98 -0
- package/src/components/schedules/ScheduleManager.stories.tsx +78 -0
- package/src/components/schedules/ScheduleManager.tsx +160 -0
- package/src/components/settings/SettingsNav.module.scss +82 -0
- package/src/components/settings/SettingsNav.stories.tsx +83 -0
- package/src/components/settings/SettingsNav.tsx +104 -0
- package/src/components/streams/StreamDetailPanel.module.scss +206 -0
- package/src/components/streams/StreamDetailPanel.stories.tsx +132 -0
- package/src/components/streams/StreamDetailPanel.tsx +119 -0
- package/src/components/streams/StreamList.module.scss +92 -0
- package/src/components/streams/StreamList.stories.tsx +99 -0
- package/src/components/streams/StreamList.tsx +114 -0
- package/src/components/streams/index.ts +10 -0
- package/src/components/tools/AgentToolCard.module.scss +118 -0
- package/src/components/tools/AgentToolCard.stories.tsx +304 -0
- package/src/components/tools/AgentToolCard.tsx +247 -0
- package/src/components/tools/FileEditCard.stories.tsx +138 -0
- package/src/components/tools/FileEditCard.tsx +160 -0
- package/src/components/tools/FileReadCard.stories.tsx +120 -0
- package/src/components/tools/FileReadCard.tsx +106 -0
- package/src/components/tools/FindingCard.stories.tsx +124 -0
- package/src/components/tools/FindingCard.tsx +178 -0
- package/src/components/tools/GenericToolCard.stories.tsx +80 -0
- package/src/components/tools/GenericToolCard.tsx +111 -0
- package/src/components/tools/IpcCard.stories.tsx +129 -0
- package/src/components/tools/IpcCard.tsx +178 -0
- package/src/components/tools/KnowledgeCard.stories.tsx +112 -0
- package/src/components/tools/KnowledgeCard.tsx +165 -0
- package/src/components/tools/MetadataCard.stories.tsx +32 -0
- package/src/components/tools/MetadataCard.tsx +39 -0
- package/src/components/tools/SearchCard.stories.tsx +74 -0
- package/src/components/tools/SearchCard.tsx +86 -0
- package/src/components/tools/ShellCard.stories.tsx +112 -0
- package/src/components/tools/ShellCard.tsx +106 -0
- package/src/components/tools/TaskCard.stories.tsx +123 -0
- package/src/components/tools/TaskCard.tsx +203 -0
- package/src/components/tools/TodoCard.module.scss +131 -0
- package/src/components/tools/TodoCard.stories.tsx +202 -0
- package/src/components/tools/TodoCard.tsx +200 -0
- package/src/components/tools/ToolCard.stories.tsx +177 -0
- package/src/components/tools/ToolCard.tsx +60 -0
- package/src/components/tools/ToolCardProps.ts +20 -0
- package/src/components/tools/ToolSearchCard.stories.tsx +81 -0
- package/src/components/tools/ToolSearchCard.tsx +86 -0
- package/src/components/tools/WorkpadCard.stories.tsx +106 -0
- package/src/components/tools/WorkpadCard.tsx +125 -0
- package/src/components/tools/classifyTool.test.ts +44 -0
- package/src/components/tools/classifyTool.ts +134 -0
- package/src/components/tools/parseDiff.ts +95 -0
- package/src/components/tools/parseShellOutput.ts +28 -0
- package/src/components/tools/toolCardHelpers.test.ts +53 -0
- package/src/components/tools/toolCards.module.scss +234 -0
- package/src/components/workspace/WorkspaceBoard.module.scss +238 -0
- package/src/components/workspace/WorkspaceBoard.stories.tsx +240 -0
- package/src/components/workspace/WorkspaceBoard.tsx +232 -0
- package/src/components/workspace/WorkspaceFormFields.module.scss +79 -0
- package/src/components/workspace/WorkspaceFormFields.stories.tsx +133 -0
- package/src/components/workspace/WorkspaceFormFields.tsx +185 -0
- package/src/context/GrackleContext.ts +28 -0
- package/src/context/GrackleContextTypes.ts +64 -0
- package/src/context/SidebarContext.tsx +53 -0
- package/src/context/ThemeContext.tsx +21 -0
- package/src/context/ToastContext.tsx +56 -0
- package/src/hooks/types.ts +864 -0
- package/src/hooks/useEventSelection.test.ts +204 -0
- package/src/hooks/useEventSelection.ts +158 -0
- package/src/hooks/useSmartScroll.ts +151 -0
- package/src/hooks/useTheme.ts +228 -0
- package/src/index.ts +210 -0
- package/src/mocks/MockGrackleProvider.tsx +1397 -0
- package/src/mocks/mockData.ts +1966 -0
- package/src/mocks/mockKnowledgeData.ts +294 -0
- package/src/scss.d.ts +12 -0
- package/src/styles/global.scss +244 -0
- package/src/styles/mixins.scss +278 -0
- package/src/styles/prism-theme.scss +148 -0
- package/src/styles/theme.scss +1102 -0
- package/src/test-utils/storybook-decorators.tsx +50 -0
- package/src/test-utils/storybook-helpers.ts +262 -0
- package/src/themes.ts +142 -0
- package/src/utils/boardColumns.ts +141 -0
- package/src/utils/breadcrumbs.test.ts +285 -0
- package/src/utils/breadcrumbs.ts +222 -0
- package/src/utils/dashboard.test.ts +156 -0
- package/src/utils/dashboard.ts +195 -0
- package/src/utils/eventContent.test.ts +353 -0
- package/src/utils/eventContent.ts +209 -0
- package/src/utils/findingCategory.ts +33 -0
- package/src/utils/format.ts +27 -0
- package/src/utils/iconSize.ts +18 -0
- package/src/utils/navigation.ts +205 -0
- package/src/utils/route-config.test.ts +128 -0
- package/src/utils/scrollUtils.test.ts +65 -0
- package/src/utils/scrollUtils.ts +49 -0
- package/src/utils/sessionEvents.test.ts +302 -0
- package/src/utils/sessionEvents.ts +233 -0
- package/src/utils/taskStatus.tsx +137 -0
- package/src/utils/time.ts +92 -0
- package/tsconfig.json +8 -0
- package/vite.config.ts +20 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure scroll-math utilities for the smart-scroll feature.
|
|
3
|
+
* No DOM or React dependencies — easy to unit test.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Pixel threshold for "near anchor" detection. */
|
|
7
|
+
export const SCROLL_ANCHOR_THRESHOLD_PX: number = 50;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Whether the scroll position is within threshold of the anchor point.
|
|
11
|
+
*
|
|
12
|
+
* @param scrollTop - Current scrollTop of the container
|
|
13
|
+
* @param scrollHeight - Total scrollable height of the container
|
|
14
|
+
* @param clientHeight - Visible height of the container
|
|
15
|
+
* @param isReversed - If true, anchor is at the top; otherwise at bottom
|
|
16
|
+
* @param threshold - Override the default 50px threshold
|
|
17
|
+
* @returns True if the scroll position is near the anchor
|
|
18
|
+
*/
|
|
19
|
+
export function isNearAnchor(
|
|
20
|
+
scrollTop: number,
|
|
21
|
+
scrollHeight: number,
|
|
22
|
+
clientHeight: number,
|
|
23
|
+
isReversed: boolean,
|
|
24
|
+
threshold: number = SCROLL_ANCHOR_THRESHOLD_PX,
|
|
25
|
+
): boolean {
|
|
26
|
+
if (isReversed) {
|
|
27
|
+
// Anchor is at the top — near anchor when scrollTop is small
|
|
28
|
+
return scrollTop < threshold;
|
|
29
|
+
}
|
|
30
|
+
// Anchor is at the bottom — near anchor when close to max scroll
|
|
31
|
+
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
|
32
|
+
return distanceFromBottom < threshold;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Compute the scrollTop delta needed to compensate for prepended content.
|
|
37
|
+
* Used in reverse mode to prevent viewport shift when new items appear at top.
|
|
38
|
+
*
|
|
39
|
+
* @param previousScrollHeight - scrollHeight before the render
|
|
40
|
+
* @param currentScrollHeight - scrollHeight after the render
|
|
41
|
+
* @returns Positive delta to add to scrollTop, or 0 if no compensation needed
|
|
42
|
+
*/
|
|
43
|
+
export function computeScrollCompensation(
|
|
44
|
+
previousScrollHeight: number,
|
|
45
|
+
currentScrollHeight: number,
|
|
46
|
+
): number {
|
|
47
|
+
const delta = currentScrollHeight - previousScrollHeight;
|
|
48
|
+
return delta > 0 ? delta : 0;
|
|
49
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { groupConsecutiveTextEvents, pairToolEvents } from "./sessionEvents.js";
|
|
3
|
+
import type { SessionEvent } from "../hooks/types.js";
|
|
4
|
+
|
|
5
|
+
/** Creates a minimal SessionEvent for testing. */
|
|
6
|
+
function makeEvent(overrides: Partial<SessionEvent> & { eventType: string }): SessionEvent {
|
|
7
|
+
return {
|
|
8
|
+
id: "evt-" + Math.random().toString(36).slice(2, 8),
|
|
9
|
+
sessionId: "session-1",
|
|
10
|
+
timestamp: new Date().toISOString(),
|
|
11
|
+
content: "",
|
|
12
|
+
...overrides,
|
|
13
|
+
} as SessionEvent;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("groupConsecutiveTextEvents", () => {
|
|
17
|
+
it("merges consecutive text events", () => {
|
|
18
|
+
const events = [
|
|
19
|
+
makeEvent({ eventType: "text", content: "Hello " }),
|
|
20
|
+
makeEvent({ eventType: "text", content: "world" }),
|
|
21
|
+
];
|
|
22
|
+
const result = groupConsecutiveTextEvents(events);
|
|
23
|
+
expect(result).toHaveLength(1);
|
|
24
|
+
expect(result[0].content).toBe("Hello world");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("does not merge non-consecutive text events", () => {
|
|
28
|
+
const events = [
|
|
29
|
+
makeEvent({ eventType: "text", content: "first" }),
|
|
30
|
+
makeEvent({ eventType: "system", content: "separator" }),
|
|
31
|
+
makeEvent({ eventType: "text", content: "second" }),
|
|
32
|
+
];
|
|
33
|
+
const result = groupConsecutiveTextEvents(events);
|
|
34
|
+
expect(result).toHaveLength(3);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("pairToolEvents", () => {
|
|
39
|
+
describe("Anthropic ID format (Claude Code)", () => {
|
|
40
|
+
it("pairs tool_use with tool_result by raw.id / raw.tool_use_id", () => {
|
|
41
|
+
const events = [
|
|
42
|
+
makeEvent({
|
|
43
|
+
eventType: "tool_use",
|
|
44
|
+
content: JSON.stringify({ tool: "Bash", args: { command: "ls" } }),
|
|
45
|
+
raw: JSON.stringify({ type: "tool_use", id: "toolu_abc123", name: "Bash" }),
|
|
46
|
+
}),
|
|
47
|
+
makeEvent({
|
|
48
|
+
eventType: "tool_result",
|
|
49
|
+
content: "file1.txt\nfile2.txt",
|
|
50
|
+
raw: JSON.stringify({ tool_use_id: "toolu_abc123", is_error: false }),
|
|
51
|
+
}),
|
|
52
|
+
];
|
|
53
|
+
const result = pairToolEvents(events);
|
|
54
|
+
// tool_use consumed, only tool_result with context remains
|
|
55
|
+
expect(result).toHaveLength(1);
|
|
56
|
+
expect(result[0].eventType).toBe("tool_result");
|
|
57
|
+
expect(result[0].toolUseCtx).toBeDefined();
|
|
58
|
+
expect(result[0].toolUseCtx!.tool).toBe("Bash");
|
|
59
|
+
expect(result[0].content).toBe("file1.txt\nfile2.txt");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("Copilot ID format", () => {
|
|
64
|
+
it("pairs tool_use with tool_result by raw.data.toolCallId", () => {
|
|
65
|
+
const events = [
|
|
66
|
+
makeEvent({
|
|
67
|
+
eventType: "tool_use",
|
|
68
|
+
content: JSON.stringify({ tool: "powershell", args: { command: "ls" } }),
|
|
69
|
+
raw: JSON.stringify({
|
|
70
|
+
type: "tool.execution_start",
|
|
71
|
+
data: { toolCallId: "call_xyz789", toolName: "powershell" },
|
|
72
|
+
}),
|
|
73
|
+
}),
|
|
74
|
+
makeEvent({
|
|
75
|
+
eventType: "tool_result",
|
|
76
|
+
content: "Directory listing...",
|
|
77
|
+
raw: JSON.stringify({
|
|
78
|
+
type: "tool.execution_complete",
|
|
79
|
+
data: { toolCallId: "call_xyz789", success: true },
|
|
80
|
+
}),
|
|
81
|
+
}),
|
|
82
|
+
];
|
|
83
|
+
const result = pairToolEvents(events);
|
|
84
|
+
expect(result).toHaveLength(1);
|
|
85
|
+
expect(result[0].eventType).toBe("tool_result");
|
|
86
|
+
expect(result[0].toolUseCtx).toBeDefined();
|
|
87
|
+
expect(result[0].toolUseCtx!.tool).toBe("powershell");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("Codex ID format", () => {
|
|
92
|
+
it("pairs tool_use with tool_result by raw.item.id", () => {
|
|
93
|
+
const events = [
|
|
94
|
+
makeEvent({
|
|
95
|
+
eventType: "tool_use",
|
|
96
|
+
content: JSON.stringify({ tool: "command_execution", args: { command: "ls" } }),
|
|
97
|
+
raw: JSON.stringify({
|
|
98
|
+
type: "item.started",
|
|
99
|
+
item: { id: "item_1", type: "command_execution" },
|
|
100
|
+
}),
|
|
101
|
+
}),
|
|
102
|
+
makeEvent({
|
|
103
|
+
eventType: "tool_result",
|
|
104
|
+
content: "[exit 0] file1.txt",
|
|
105
|
+
raw: JSON.stringify({
|
|
106
|
+
type: "item.completed",
|
|
107
|
+
item: { id: "item_1", type: "command_execution" },
|
|
108
|
+
}),
|
|
109
|
+
}),
|
|
110
|
+
];
|
|
111
|
+
const result = pairToolEvents(events);
|
|
112
|
+
expect(result).toHaveLength(1);
|
|
113
|
+
expect(result[0].eventType).toBe("tool_result");
|
|
114
|
+
expect(result[0].toolUseCtx).toBeDefined();
|
|
115
|
+
expect(result[0].toolUseCtx!.tool).toBe("command_execution");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("adjacent fallback pairing", () => {
|
|
120
|
+
it("pairs unpaired tool_use with immediately adjacent tool_result", () => {
|
|
121
|
+
const events = [
|
|
122
|
+
makeEvent({
|
|
123
|
+
eventType: "tool_use",
|
|
124
|
+
content: JSON.stringify({ tool: "Bash", args: { command: "echo hi" } }),
|
|
125
|
+
// No raw metadata — cannot pair by ID
|
|
126
|
+
}),
|
|
127
|
+
makeEvent({
|
|
128
|
+
eventType: "tool_result",
|
|
129
|
+
content: "hi",
|
|
130
|
+
// No raw metadata — cannot pair by ID
|
|
131
|
+
}),
|
|
132
|
+
];
|
|
133
|
+
const result = pairToolEvents(events);
|
|
134
|
+
expect(result).toHaveLength(1);
|
|
135
|
+
expect(result[0].eventType).toBe("tool_result");
|
|
136
|
+
expect(result[0].toolUseCtx).toBeDefined();
|
|
137
|
+
expect(result[0].toolUseCtx!.tool).toBe("Bash");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("does not pair tool_result that appears before tool_use", () => {
|
|
141
|
+
const events = [
|
|
142
|
+
makeEvent({
|
|
143
|
+
eventType: "tool_result",
|
|
144
|
+
content: "orphan result",
|
|
145
|
+
}),
|
|
146
|
+
makeEvent({
|
|
147
|
+
eventType: "tool_use",
|
|
148
|
+
content: JSON.stringify({ tool: "Bash", args: { command: "ls" } }),
|
|
149
|
+
}),
|
|
150
|
+
];
|
|
151
|
+
const result = pairToolEvents(events);
|
|
152
|
+
// Both remain unpaired
|
|
153
|
+
expect(result).toHaveLength(2);
|
|
154
|
+
expect(result[0].eventType).toBe("tool_result");
|
|
155
|
+
expect(result[0].toolUseCtx).toBeUndefined();
|
|
156
|
+
expect(result[1].eventType).toBe("tool_use");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("does not pair non-adjacent tool_use and tool_result", () => {
|
|
160
|
+
const events = [
|
|
161
|
+
makeEvent({
|
|
162
|
+
eventType: "tool_use",
|
|
163
|
+
content: JSON.stringify({ tool: "Bash", args: { command: "ls" } }),
|
|
164
|
+
}),
|
|
165
|
+
makeEvent({
|
|
166
|
+
eventType: "text",
|
|
167
|
+
content: "some text in between",
|
|
168
|
+
}),
|
|
169
|
+
makeEvent({
|
|
170
|
+
eventType: "tool_result",
|
|
171
|
+
content: "result",
|
|
172
|
+
}),
|
|
173
|
+
];
|
|
174
|
+
const result = pairToolEvents(events);
|
|
175
|
+
// All three remain (tool_use not consumed, text stays, tool_result unpaired)
|
|
176
|
+
expect(result).toHaveLength(3);
|
|
177
|
+
expect(result[0].eventType).toBe("tool_use");
|
|
178
|
+
expect(result[2].eventType).toBe("tool_result");
|
|
179
|
+
expect(result[2].toolUseCtx).toBeUndefined();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("multiple tool calls", () => {
|
|
184
|
+
it("pairs multiple Codex tool calls correctly", () => {
|
|
185
|
+
const events = [
|
|
186
|
+
makeEvent({
|
|
187
|
+
eventType: "tool_use",
|
|
188
|
+
content: JSON.stringify({ tool: "command_execution", args: { command: "ls" } }),
|
|
189
|
+
raw: JSON.stringify({ type: "item.started", item: { id: "item_1" } }),
|
|
190
|
+
}),
|
|
191
|
+
makeEvent({
|
|
192
|
+
eventType: "tool_result",
|
|
193
|
+
content: "[exit 0] files...",
|
|
194
|
+
raw: JSON.stringify({ type: "item.completed", item: { id: "item_1" } }),
|
|
195
|
+
}),
|
|
196
|
+
makeEvent({
|
|
197
|
+
eventType: "tool_use",
|
|
198
|
+
content: JSON.stringify({ tool: "mcp__grackle__workpad_write", args: { status: "done" } }),
|
|
199
|
+
raw: JSON.stringify({ type: "item.started", item: { id: "item_2" } }),
|
|
200
|
+
}),
|
|
201
|
+
makeEvent({
|
|
202
|
+
eventType: "tool_result",
|
|
203
|
+
content: '{"taskId":"abc"}',
|
|
204
|
+
raw: JSON.stringify({ type: "item.completed", item: { id: "item_2" } }),
|
|
205
|
+
}),
|
|
206
|
+
];
|
|
207
|
+
const result = pairToolEvents(events);
|
|
208
|
+
expect(result).toHaveLength(2);
|
|
209
|
+
expect(result[0].toolUseCtx!.tool).toBe("command_execution");
|
|
210
|
+
expect(result[1].toolUseCtx!.tool).toBe("mcp__grackle__workpad_write");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("detailedResult extraction", () => {
|
|
215
|
+
it("extracts detailedContent from Copilot tool result JSON", () => {
|
|
216
|
+
const events = [
|
|
217
|
+
makeEvent({
|
|
218
|
+
eventType: "tool_use",
|
|
219
|
+
content: JSON.stringify({ tool: "edit", args: { file: "test.ts" } }),
|
|
220
|
+
raw: JSON.stringify({ data: { toolCallId: "call_1" } }),
|
|
221
|
+
}),
|
|
222
|
+
makeEvent({
|
|
223
|
+
eventType: "tool_result",
|
|
224
|
+
content: JSON.stringify({ content: "Applied changes", detailedContent: "--- a/test.ts\n+++ b/test.ts\n@@ -1 +1 @@\n-old\n+new" }),
|
|
225
|
+
raw: JSON.stringify({ data: { toolCallId: "call_1" } }),
|
|
226
|
+
}),
|
|
227
|
+
];
|
|
228
|
+
const result = pairToolEvents(events);
|
|
229
|
+
expect(result).toHaveLength(1);
|
|
230
|
+
expect(result[0].toolUseCtx!.detailedResult).toBe("--- a/test.ts\n+++ b/test.ts\n@@ -1 +1 @@\n-old\n+new");
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe("unpaired tool_use (Claude Code text-result pattern)", () => {
|
|
235
|
+
it("leaves tool_use in display when no tool_result exists", () => {
|
|
236
|
+
const events = [
|
|
237
|
+
makeEvent({
|
|
238
|
+
eventType: "tool_use",
|
|
239
|
+
content: JSON.stringify({ tool: "Bash", args: { command: "ls" } }),
|
|
240
|
+
raw: JSON.stringify({ type: "tool_use", id: "toolu_abc" }),
|
|
241
|
+
}),
|
|
242
|
+
makeEvent({
|
|
243
|
+
eventType: "text",
|
|
244
|
+
content: "```\nfile1.txt\nfile2.txt\n```",
|
|
245
|
+
}),
|
|
246
|
+
];
|
|
247
|
+
const result = pairToolEvents(events);
|
|
248
|
+
// tool_use remains (not consumed), text remains
|
|
249
|
+
expect(result).toHaveLength(2);
|
|
250
|
+
expect(result[0].eventType).toBe("tool_use");
|
|
251
|
+
expect(result[1].eventType).toBe("text");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("marks unpaired tool_use as settled when subsequent events exist", () => {
|
|
255
|
+
const events = [
|
|
256
|
+
makeEvent({
|
|
257
|
+
eventType: "tool_use",
|
|
258
|
+
content: JSON.stringify({ tool: "Bash", args: { command: "ls" } }),
|
|
259
|
+
raw: JSON.stringify({ type: "tool_use", id: "toolu_abc" }),
|
|
260
|
+
}),
|
|
261
|
+
makeEvent({
|
|
262
|
+
eventType: "text",
|
|
263
|
+
content: "```\nfile1.txt\n```",
|
|
264
|
+
}),
|
|
265
|
+
];
|
|
266
|
+
const result = pairToolEvents(events);
|
|
267
|
+
expect(result[0].settled).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("does not mark tool_use as settled when it is the last event (still running)", () => {
|
|
271
|
+
const events = [
|
|
272
|
+
makeEvent({
|
|
273
|
+
eventType: "tool_use",
|
|
274
|
+
content: JSON.stringify({ tool: "Bash", args: { command: "npm install" } }),
|
|
275
|
+
raw: JSON.stringify({ type: "tool_use", id: "toolu_xyz" }),
|
|
276
|
+
}),
|
|
277
|
+
];
|
|
278
|
+
const result = pairToolEvents(events);
|
|
279
|
+
expect(result).toHaveLength(1);
|
|
280
|
+
expect(result[0].settled).toBeUndefined();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("does not mark tool_use as settled when only more tool_use events follow", () => {
|
|
284
|
+
const events = [
|
|
285
|
+
makeEvent({
|
|
286
|
+
eventType: "tool_use",
|
|
287
|
+
content: JSON.stringify({ tool: "Bash", args: { command: "ls" } }),
|
|
288
|
+
raw: JSON.stringify({ type: "tool_use", id: "toolu_1" }),
|
|
289
|
+
}),
|
|
290
|
+
makeEvent({
|
|
291
|
+
eventType: "tool_use",
|
|
292
|
+
content: JSON.stringify({ tool: "Bash", args: { command: "pwd" } }),
|
|
293
|
+
raw: JSON.stringify({ type: "tool_use", id: "toolu_2" }),
|
|
294
|
+
}),
|
|
295
|
+
];
|
|
296
|
+
const result = pairToolEvents(events);
|
|
297
|
+
expect(result).toHaveLength(2);
|
|
298
|
+
expect(result[0].settled).toBeUndefined();
|
|
299
|
+
expect(result[1].settled).toBeUndefined();
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import type { SessionEvent } from "../hooks/types.js";
|
|
2
|
+
|
|
3
|
+
/** Session event augmented with optional tool_use context for paired tool results. */
|
|
4
|
+
export type DisplayEvent = SessionEvent & {
|
|
5
|
+
toolUseCtx?: { tool: string; args: unknown; detailedResult?: string };
|
|
6
|
+
/**
|
|
7
|
+
* True when a tool_use event has no matching tool_result but subsequent events
|
|
8
|
+
* prove the tool completed (e.g. Claude Code emits results as text, not tool_result).
|
|
9
|
+
* EventRenderer uses this to avoid showing a misleading in-progress spinner.
|
|
10
|
+
*/
|
|
11
|
+
settled?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Merges consecutive "text" events into single entries with concatenated content. */
|
|
15
|
+
export function groupConsecutiveTextEvents(events: SessionEvent[]): SessionEvent[] {
|
|
16
|
+
const result: SessionEvent[] = [];
|
|
17
|
+
for (const event of events) {
|
|
18
|
+
const previous = result[result.length - 1];
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- previous is undefined on first iteration
|
|
20
|
+
if (event.eventType === "text" && previous?.eventType === "text") {
|
|
21
|
+
result[result.length - 1] = { ...previous, content: previous.content + event.content };
|
|
22
|
+
} else {
|
|
23
|
+
result.push(event);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extracts the tool-use ID from a tool_use event's raw metadata.
|
|
31
|
+
*
|
|
32
|
+
* Different runtimes store the ID in different locations:
|
|
33
|
+
* - Claude Code (Anthropic SDK): `raw.id` (e.g. "toolu_...")
|
|
34
|
+
* - Copilot: `raw.data.toolCallId` (e.g. "call_...")
|
|
35
|
+
* - Codex: `raw.item.id` (e.g. "item_1")
|
|
36
|
+
*/
|
|
37
|
+
function extractToolUseId(raw: Record<string, unknown>): string | undefined {
|
|
38
|
+
if (typeof raw.id === "string") {
|
|
39
|
+
return raw.id;
|
|
40
|
+
}
|
|
41
|
+
const data = raw.data as Record<string, unknown> | undefined;
|
|
42
|
+
if (data && typeof data.toolCallId === "string") {
|
|
43
|
+
return data.toolCallId;
|
|
44
|
+
}
|
|
45
|
+
const item = raw.item as Record<string, unknown> | undefined;
|
|
46
|
+
if (item && typeof item.id === "string") {
|
|
47
|
+
return item.id;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extracts the tool-use ID from a tool_result event's raw metadata.
|
|
54
|
+
*
|
|
55
|
+
* Different runtimes store the back-reference in different locations:
|
|
56
|
+
* - Claude Code (Anthropic SDK): `raw.tool_use_id`
|
|
57
|
+
* - Copilot: `raw.data.toolCallId`
|
|
58
|
+
* - Codex: `raw.item.id`
|
|
59
|
+
*/
|
|
60
|
+
function extractToolResultId(raw: Record<string, unknown>): string | undefined {
|
|
61
|
+
if (typeof raw.tool_use_id === "string") {
|
|
62
|
+
return raw.tool_use_id;
|
|
63
|
+
}
|
|
64
|
+
const data = raw.data as Record<string, unknown> | undefined;
|
|
65
|
+
if (data && typeof data.toolCallId === "string") {
|
|
66
|
+
return data.toolCallId;
|
|
67
|
+
}
|
|
68
|
+
const item = raw.item as Record<string, unknown> | undefined;
|
|
69
|
+
if (item && typeof item.id === "string") {
|
|
70
|
+
return item.id;
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Pairs tool_use events with their tool_result counterparts. */
|
|
76
|
+
export function pairToolEvents(events: SessionEvent[]): DisplayEvent[] {
|
|
77
|
+
const parsedRaw = new Map<SessionEvent, Record<string, unknown>>();
|
|
78
|
+
for (const e of events) {
|
|
79
|
+
if (!e.raw) continue;
|
|
80
|
+
try {
|
|
81
|
+
parsedRaw.set(e, JSON.parse(e.raw) as Record<string, unknown>);
|
|
82
|
+
} catch { /* skip unparseable events */ }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Build a map of tool_use IDs → context, supporting all runtime ID formats.
|
|
86
|
+
const toolUseById = new Map<string, { tool: string; args: unknown }>();
|
|
87
|
+
for (const e of events) {
|
|
88
|
+
if (e.eventType !== "tool_use") continue;
|
|
89
|
+
const raw = parsedRaw.get(e);
|
|
90
|
+
const id = raw ? extractToolUseId(raw) : undefined;
|
|
91
|
+
if (!id) continue;
|
|
92
|
+
try {
|
|
93
|
+
const content = JSON.parse(e.content) as { tool: string; args: unknown };
|
|
94
|
+
toolUseById.set(id, { tool: content.tool, args: content.args });
|
|
95
|
+
} catch { /* skip unparseable events */ }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Phase 1: ID-based pairing — match tool_result events to tool_use by ID.
|
|
99
|
+
const consumedIds = new Set<string>();
|
|
100
|
+
const pairedResultIndices = new Set<number>();
|
|
101
|
+
const display: DisplayEvent[] = events.map((e, index) => {
|
|
102
|
+
if (e.eventType !== "tool_result") return e;
|
|
103
|
+
const raw = parsedRaw.get(e);
|
|
104
|
+
const resultId = raw ? extractToolResultId(raw) : undefined;
|
|
105
|
+
if (!resultId) return e;
|
|
106
|
+
const ctx = toolUseById.get(resultId);
|
|
107
|
+
if (!ctx) return e;
|
|
108
|
+
consumedIds.add(resultId);
|
|
109
|
+
pairedResultIndices.add(index);
|
|
110
|
+
|
|
111
|
+
// Extract detailedResult from content when it's a JSON object with detailedContent
|
|
112
|
+
// (Copilot emits tool results in this format with embedded diffs).
|
|
113
|
+
// Guard with startsWith check to avoid throwing on plain text / large outputs.
|
|
114
|
+
let detailedResult: string | undefined;
|
|
115
|
+
const contentStr: string = e.content.trim();
|
|
116
|
+
if (contentStr.startsWith("{")) {
|
|
117
|
+
try {
|
|
118
|
+
const parsed = JSON.parse(contentStr) as Record<string, unknown>;
|
|
119
|
+
if (typeof parsed.detailedContent === "string") {
|
|
120
|
+
detailedResult = parsed.detailedContent;
|
|
121
|
+
}
|
|
122
|
+
} catch { /* content looks like JSON but isn't — skip */ }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { ...e, toolUseCtx: { ...ctx, detailedResult } };
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Phase 2: Adjacent fallback — pair remaining unpaired tool_use with an immediately
|
|
129
|
+
// adjacent unpaired tool_result. Only pairs when the tool_result directly follows
|
|
130
|
+
// the tool_use (no events between them) to avoid mis-pairing in async scenarios.
|
|
131
|
+
const unpairedToolUseIndices: number[] = [];
|
|
132
|
+
for (let i = 0; i < display.length; i++) {
|
|
133
|
+
if (display[i].eventType !== "tool_use") continue;
|
|
134
|
+
const raw = parsedRaw.get(display[i]);
|
|
135
|
+
const id = raw ? extractToolUseId(raw) : undefined;
|
|
136
|
+
if (id && consumedIds.has(id)) continue;
|
|
137
|
+
unpairedToolUseIndices.push(i);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const unpairedResultIndices: number[] = [];
|
|
141
|
+
for (let i = 0; i < display.length; i++) {
|
|
142
|
+
if (display[i].eventType !== "tool_result" || pairedResultIndices.has(i)) continue;
|
|
143
|
+
unpairedResultIndices.push(i);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Match unpaired tool_use to an immediately adjacent unpaired tool_result.
|
|
147
|
+
let resultCursor = 0;
|
|
148
|
+
for (const useIdx of unpairedToolUseIndices) {
|
|
149
|
+
// Advance cursor past results that appear before this tool_use
|
|
150
|
+
while (resultCursor < unpairedResultIndices.length && unpairedResultIndices[resultCursor] < useIdx) {
|
|
151
|
+
resultCursor++;
|
|
152
|
+
}
|
|
153
|
+
if (resultCursor >= unpairedResultIndices.length) break;
|
|
154
|
+
|
|
155
|
+
const resultIdx = unpairedResultIndices[resultCursor];
|
|
156
|
+
// Only pair if the tool_result is the immediately next event (adjacent)
|
|
157
|
+
if (resultIdx !== useIdx + 1) continue;
|
|
158
|
+
const useEvent = display[useIdx];
|
|
159
|
+
const resultEvent = display[resultIdx];
|
|
160
|
+
|
|
161
|
+
let ctx: { tool: string; args: unknown } | undefined;
|
|
162
|
+
try {
|
|
163
|
+
const content = JSON.parse(useEvent.content) as { tool: string; args: unknown };
|
|
164
|
+
ctx = { tool: content.tool, args: content.args };
|
|
165
|
+
} catch { /* skip */ }
|
|
166
|
+
|
|
167
|
+
if (ctx) {
|
|
168
|
+
// Extract detailedResult
|
|
169
|
+
let detailedResult: string | undefined;
|
|
170
|
+
const contentStr: string = resultEvent.content.trim();
|
|
171
|
+
if (contentStr.startsWith("{")) {
|
|
172
|
+
try {
|
|
173
|
+
const parsed = JSON.parse(contentStr) as Record<string, unknown>;
|
|
174
|
+
if (typeof parsed.detailedContent === "string") {
|
|
175
|
+
detailedResult = parsed.detailedContent;
|
|
176
|
+
}
|
|
177
|
+
} catch { /* skip */ }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
display[resultIdx] = { ...resultEvent, toolUseCtx: { ...ctx, detailedResult } };
|
|
181
|
+
pairedResultIndices.add(resultIdx);
|
|
182
|
+
|
|
183
|
+
// Mark the tool_use as consumed so it's filtered out below
|
|
184
|
+
const raw = parsedRaw.get(useEvent);
|
|
185
|
+
const id = raw ? extractToolUseId(raw) : undefined;
|
|
186
|
+
if (id) {
|
|
187
|
+
consumedIds.add(id);
|
|
188
|
+
} else {
|
|
189
|
+
// No ID available — use a synthetic marker to track consumption
|
|
190
|
+
consumedIds.add(`__seq_${useIdx}`);
|
|
191
|
+
// Store the synthetic ID so the filter below can find it
|
|
192
|
+
parsedRaw.set(useEvent, { ...(raw ?? {}), __seqId: `__seq_${useIdx}` });
|
|
193
|
+
}
|
|
194
|
+
resultCursor++;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Filter out consumed tool_use events (their info is now embedded in tool_result).
|
|
199
|
+
const filtered = display.filter((e) => {
|
|
200
|
+
if (e.eventType !== "tool_use") return true;
|
|
201
|
+
const raw = parsedRaw.get(e);
|
|
202
|
+
if (!raw) return true;
|
|
203
|
+
const id = extractToolUseId(raw);
|
|
204
|
+
if (id && consumedIds.has(id)) return false;
|
|
205
|
+
// Check synthetic sequential marker
|
|
206
|
+
const seqId = raw.__seqId as string | undefined;
|
|
207
|
+
if (seqId && consumedIds.has(seqId)) return false;
|
|
208
|
+
return true;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Phase 3: Mark remaining unpaired tool_use events as "settled" if subsequent
|
|
212
|
+
// events prove the tool completed. This handles runtimes like Claude Code that
|
|
213
|
+
// emit tool results as text events rather than tool_result events — without this,
|
|
214
|
+
// the ShellCard shows a misleading in-progress spinner forever.
|
|
215
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
216
|
+
if (filtered[i].eventType !== "tool_use") continue;
|
|
217
|
+
// Only settle if there is at least one subsequent non-tool_use event.
|
|
218
|
+
// This avoids prematurely settling in multi-tool sequences where only
|
|
219
|
+
// more tool_use events follow (the tools may still be running).
|
|
220
|
+
let hasNonToolUseAfter = false;
|
|
221
|
+
for (let j = i + 1; j < filtered.length; j++) {
|
|
222
|
+
if (filtered[j].eventType !== "tool_use") {
|
|
223
|
+
hasNonToolUseAfter = true;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (hasNonToolUseAfter) {
|
|
228
|
+
filtered[i] = { ...filtered[i], settled: true };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return filtered;
|
|
233
|
+
}
|