@athenaintel/react 0.48.0 → 0.52.1
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/README.md +68 -10
- package/dist/agent-runtime/src/call-recap.cjs +319 -0
- package/dist/agent-runtime/src/call-recap.cjs.map +1 -0
- package/dist/agent-runtime/src/call-recap.d.ts +142 -0
- package/dist/agent-runtime/src/call-recap.js +319 -0
- package/dist/agent-runtime/src/call-recap.js.map +1 -0
- package/dist/agent-runtime/src/chat-outbox-store.cjs +1 -0
- package/dist/agent-runtime/src/chat-outbox-store.cjs.map +1 -1
- package/dist/agent-runtime/src/chat-outbox-store.d.ts +7 -0
- package/dist/agent-runtime/src/chat-outbox-store.js +1 -0
- package/dist/agent-runtime/src/chat-outbox-store.js.map +1 -1
- package/dist/agent-runtime/src/chat-outbox.cjs +46 -5
- package/dist/agent-runtime/src/chat-outbox.cjs.map +1 -1
- package/dist/agent-runtime/src/chat-outbox.d.ts +29 -0
- package/dist/agent-runtime/src/chat-outbox.js +46 -5
- package/dist/agent-runtime/src/chat-outbox.js.map +1 -1
- package/dist/agent-runtime/src/conversation-index.cjs.map +1 -1
- package/dist/agent-runtime/src/conversation-index.d.ts +7 -0
- package/dist/agent-runtime/src/conversation-index.js.map +1 -1
- package/dist/agent-runtime/src/conversation-list.cjs +2 -1
- package/dist/agent-runtime/src/conversation-list.cjs.map +1 -1
- package/dist/agent-runtime/src/conversation-list.d.ts +19 -3
- package/dist/agent-runtime/src/conversation-list.js +2 -1
- package/dist/agent-runtime/src/conversation-list.js.map +1 -1
- package/dist/agent-runtime/src/desktop-handoff.d.ts +10 -0
- package/dist/agent-runtime/src/edit-command.cjs +21 -0
- package/dist/agent-runtime/src/edit-command.cjs.map +1 -1
- package/dist/agent-runtime/src/edit-command.d.ts +8 -0
- package/dist/agent-runtime/src/edit-command.js +21 -0
- package/dist/agent-runtime/src/edit-command.js.map +1 -1
- package/dist/agent-runtime/src/handoff-arrival.cjs +19 -0
- package/dist/agent-runtime/src/handoff-arrival.cjs.map +1 -1
- package/dist/agent-runtime/src/handoff-arrival.d.ts +18 -3
- package/dist/agent-runtime/src/handoff-arrival.js +20 -1
- package/dist/agent-runtime/src/handoff-arrival.js.map +1 -1
- package/dist/agent-runtime/src/handoff-cursor-id.cjs +6 -0
- package/dist/agent-runtime/src/handoff-cursor-id.cjs.map +1 -0
- package/dist/agent-runtime/src/handoff-cursor-id.d.ts +12 -0
- package/dist/agent-runtime/src/handoff-cursor-id.js +6 -0
- package/dist/agent-runtime/src/handoff-cursor-id.js.map +1 -0
- package/dist/agent-runtime/src/handoff-cursor.cjs +88 -0
- package/dist/agent-runtime/src/handoff-cursor.cjs.map +1 -0
- package/dist/agent-runtime/src/handoff-cursor.d.ts +120 -0
- package/dist/agent-runtime/src/handoff-cursor.js +88 -0
- package/dist/agent-runtime/src/handoff-cursor.js.map +1 -0
- package/dist/agent-runtime/src/index.d.ts +9 -7
- package/dist/agent-runtime/src/interrupts.cjs +28 -1
- package/dist/agent-runtime/src/interrupts.cjs.map +1 -1
- package/dist/agent-runtime/src/interrupts.d.ts +21 -1
- package/dist/agent-runtime/src/interrupts.js +28 -1
- package/dist/agent-runtime/src/interrupts.js.map +1 -1
- package/dist/agent-runtime/src/model-context.cjs +3 -2
- package/dist/agent-runtime/src/model-context.cjs.map +1 -1
- package/dist/agent-runtime/src/model-context.d.ts +6 -1
- package/dist/agent-runtime/src/model-context.js +3 -2
- package/dist/agent-runtime/src/model-context.js.map +1 -1
- package/dist/agent-runtime/src/runtime.d.ts +3 -2
- package/dist/agent-runtime/src/send-recovery.cjs +37 -3
- package/dist/agent-runtime/src/send-recovery.cjs.map +1 -1
- package/dist/agent-runtime/src/send-recovery.d.ts +19 -0
- package/dist/agent-runtime/src/send-recovery.js +37 -3
- package/dist/agent-runtime/src/send-recovery.js.map +1 -1
- package/dist/agent-runtime/src/thread-handoff-flow-machine.d.ts +14 -8
- package/dist/agent-runtime/src/thread-handoff-protocol.d.ts +227 -5
- package/dist/agent-runtime/src/thread-handoff.d.ts +1 -1
- package/dist/agent-runtime/src/use-conversation-list.cjs +3 -2
- package/dist/agent-runtime/src/use-conversation-list.cjs.map +1 -1
- package/dist/agent-runtime/src/use-conversation-list.d.ts +6 -0
- package/dist/agent-runtime/src/use-conversation-list.js +3 -2
- package/dist/agent-runtime/src/use-conversation-list.js.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-chat-outbox.cjs +21 -1
- package/dist/agent-runtime/src/use-deep-agent-chat-outbox.cjs.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-chat-outbox.d.ts +8 -2
- package/dist/agent-runtime/src/use-deep-agent-chat-outbox.js +21 -1
- package/dist/agent-runtime/src/use-deep-agent-chat-outbox.js.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-runtime.cjs +24 -6
- package/dist/agent-runtime/src/use-deep-agent-runtime.cjs.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-runtime.d.ts +17 -0
- package/dist/agent-runtime/src/use-deep-agent-runtime.js +24 -6
- package/dist/agent-runtime/src/use-deep-agent-runtime.js.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-voice.cjs +44 -9
- package/dist/agent-runtime/src/use-deep-agent-voice.cjs.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-voice.d.ts +34 -1
- package/dist/agent-runtime/src/use-deep-agent-voice.js +44 -9
- package/dist/agent-runtime/src/use-deep-agent-voice.js.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs +8 -3
- package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-window-sync.d.ts +7 -0
- package/dist/agent-runtime/src/use-deep-agent-window-sync.js +9 -4
- package/dist/agent-runtime/src/use-deep-agent-window-sync.js.map +1 -1
- package/dist/agent-runtime/src/use-thread-handoff-flow.d.ts +13 -2
- package/dist/agent-runtime/src/voice-earcon.cjs +139 -0
- package/dist/agent-runtime/src/voice-earcon.cjs.map +1 -0
- package/dist/agent-runtime/src/voice-earcon.d.ts +38 -0
- package/dist/agent-runtime/src/voice-earcon.js +139 -0
- package/dist/agent-runtime/src/voice-earcon.js.map +1 -0
- package/dist/agent-runtime/src/voice-media.cjs.map +1 -1
- package/dist/agent-runtime/src/voice-media.d.ts +14 -0
- package/dist/agent-runtime/src/voice-media.js.map +1 -1
- package/dist/agent-runtime/src/voice-presentation.cjs +2 -0
- package/dist/agent-runtime/src/voice-presentation.cjs.map +1 -1
- package/dist/agent-runtime/src/voice-presentation.js +2 -0
- package/dist/agent-runtime/src/voice-presentation.js.map +1 -1
- package/dist/assets/AthenaAssetEmbed.cjs.map +1 -1
- package/dist/assets/AthenaAssetEmbed.d.ts +11 -5
- package/dist/assets/AthenaAssetEmbed.js.map +1 -1
- package/dist/assets/use-asset-embed.cjs.map +1 -1
- package/dist/assets/use-asset-embed.d.ts +12 -5
- package/dist/assets/use-asset-embed.js.map +1 -1
- package/dist/chat/AthenaChat.cjs +27 -12
- package/dist/chat/AthenaChat.cjs.map +1 -1
- package/dist/chat/AthenaChat.d.ts +13 -0
- package/dist/chat/AthenaChat.js +27 -12
- package/dist/chat/AthenaChat.js.map +1 -1
- package/dist/chat/AthenaPlanStrip.cjs +97 -0
- package/dist/chat/AthenaPlanStrip.cjs.map +1 -0
- package/dist/chat/AthenaPlanStrip.d.ts +15 -0
- package/dist/chat/AthenaPlanStrip.js +97 -0
- package/dist/chat/AthenaPlanStrip.js.map +1 -0
- package/dist/chat/StatewireApprovalCard.cjs +41 -6
- package/dist/chat/StatewireApprovalCard.cjs.map +1 -1
- package/dist/chat/StatewireApprovalCard.d.ts +18 -1
- package/dist/chat/StatewireApprovalCard.js +42 -7
- package/dist/chat/StatewireApprovalCard.js.map +1 -1
- package/dist/chat/StatewireQueuedMessages.cjs +56 -6
- package/dist/chat/StatewireQueuedMessages.cjs.map +1 -1
- package/dist/chat/StatewireQueuedMessages.d.ts +14 -0
- package/dist/chat/StatewireQueuedMessages.js +58 -8
- package/dist/chat/StatewireQueuedMessages.js.map +1 -1
- package/dist/chat/WaitingOnYouBanner.cjs +6 -2
- package/dist/chat/WaitingOnYouBanner.cjs.map +1 -1
- package/dist/chat/WaitingOnYouBanner.js +7 -3
- package/dist/chat/WaitingOnYouBanner.js.map +1 -1
- package/dist/chat/chat-voice-controls.cjs +3 -1
- package/dist/chat/chat-voice-controls.cjs.map +1 -1
- package/dist/chat/chat-voice-controls.d.ts +6 -1
- package/dist/chat/chat-voice-controls.js +3 -1
- package/dist/chat/chat-voice-controls.js.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingCard.cjs +4 -2
- package/dist/chat/super-grouping/SuperGroupingCard.cjs.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingCard.js +4 -2
- package/dist/chat/super-grouping/SuperGroupingCard.js.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingTranscript.cjs +4 -2
- package/dist/chat/super-grouping/SuperGroupingTranscript.cjs.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingTranscript.js +4 -2
- package/dist/chat/super-grouping/SuperGroupingTranscript.js.map +1 -1
- package/dist/chat/super-grouping/activity-timeline.cjs +6 -4
- package/dist/chat/super-grouping/activity-timeline.cjs.map +1 -1
- package/dist/chat/super-grouping/activity-timeline.d.ts +9 -1
- package/dist/chat/super-grouping/activity-timeline.js +6 -4
- package/dist/chat/super-grouping/activity-timeline.js.map +1 -1
- package/dist/chat/super-grouping/index.d.ts +1 -1
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.cjs +17 -0
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.cjs.map +1 -0
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.d.ts +8 -0
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.js +17 -0
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.js.map +1 -0
- package/dist/chat-ui/index.cjs +11 -0
- package/dist/chat-ui/index.cjs.map +1 -1
- package/dist/chat-ui/index.d.ts +3 -1
- package/dist/chat-ui/index.js +12 -1
- package/dist/chat-ui/index.js.map +1 -1
- package/dist/chat-ui/messages/from-the-call-card.cjs +124 -0
- package/dist/chat-ui/messages/from-the-call-card.cjs.map +1 -0
- package/dist/chat-ui/messages/from-the-call-card.d.ts +17 -0
- package/dist/chat-ui/messages/from-the-call-card.js +124 -0
- package/dist/chat-ui/messages/from-the-call-card.js.map +1 -0
- package/dist/chat-ui/rich-text/chat-link.cjs +1 -0
- package/dist/chat-ui/rich-text/chat-link.cjs.map +1 -1
- package/dist/chat-ui/rich-text/chat-link.d.ts +4 -0
- package/dist/chat-ui/rich-text/chat-link.js +4 -3
- package/dist/chat-ui/rich-text/chat-link.js.map +1 -1
- package/dist/chat-ui/rich-text/chat-message-body.cjs +1 -0
- package/dist/chat-ui/rich-text/chat-message-body.cjs.map +1 -1
- package/dist/chat-ui/rich-text/chat-message-body.d.ts +12 -0
- package/dist/chat-ui/rich-text/chat-message-body.js +2 -1
- package/dist/chat-ui/rich-text/chat-message-body.js.map +1 -1
- package/dist/chat-ui/thread/chat-thread-shell.cjs +4 -0
- package/dist/chat-ui/thread/chat-thread-shell.cjs.map +1 -1
- package/dist/chat-ui/thread/chat-thread-shell.d.ts +4 -0
- package/dist/chat-ui/thread/chat-thread-shell.js +4 -0
- package/dist/chat-ui/thread/chat-thread-shell.js.map +1 -1
- package/dist/chat-ui/thread/handoff-cursor.cjs +216 -0
- package/dist/chat-ui/thread/handoff-cursor.cjs.map +1 -0
- package/dist/chat-ui/thread/handoff-cursor.d.ts +57 -0
- package/dist/chat-ui/thread/handoff-cursor.js +216 -0
- package/dist/chat-ui/thread/handoff-cursor.js.map +1 -0
- package/dist/chat-ui/thread/statewire-history-edge.cjs +2 -0
- package/dist/chat-ui/thread/statewire-history-edge.cjs.map +1 -1
- package/dist/chat-ui/thread/statewire-history-edge.d.ts +2 -0
- package/dist/chat-ui/thread/statewire-history-edge.js +3 -1
- package/dist/chat-ui/thread/statewire-history-edge.js.map +1 -1
- package/dist/collab/client.cjs +15 -3
- package/dist/collab/client.cjs.map +1 -1
- package/dist/collab/client.d.ts +4 -2
- package/dist/collab/client.js +16 -4
- package/dist/collab/client.js.map +1 -1
- package/dist/composer/index.d.ts +1 -1
- package/dist/composer/useReadComposerDraft.cjs +26 -7
- package/dist/composer/useReadComposerDraft.cjs.map +1 -1
- package/dist/composer/useReadComposerDraft.d.ts +23 -1
- package/dist/composer/useReadComposerDraft.js +27 -8
- package/dist/composer/useReadComposerDraft.js.map +1 -1
- package/dist/index.cjs +5 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/keryx-http/src/unconfirmed-update-attributions.cjs +207 -0
- package/dist/keryx-http/src/unconfirmed-update-attributions.cjs.map +1 -0
- package/dist/keryx-http/src/unconfirmed-update-attributions.d.ts +141 -0
- package/dist/keryx-http/src/unconfirmed-update-attributions.js +207 -0
- package/dist/keryx-http/src/unconfirmed-update-attributions.js.map +1 -0
- package/dist/keryx-http/src/update-attributions.cjs +34 -4
- package/dist/keryx-http/src/update-attributions.cjs.map +1 -1
- package/dist/keryx-http/src/update-attributions.d.ts +36 -4
- package/dist/keryx-http/src/update-attributions.js +35 -5
- package/dist/keryx-http/src/update-attributions.js.map +1 -1
- package/dist/provider/AthenaProvider.cjs +4 -0
- package/dist/provider/AthenaProvider.cjs.map +1 -1
- package/dist/provider/AthenaProvider.d.ts +11 -1
- package/dist/provider/AthenaProvider.js +4 -0
- package/dist/provider/AthenaProvider.js.map +1 -1
- package/dist/react/package.json.cjs +1 -1
- package/dist/react/package.json.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/threads/api.cjs.map +1 -1
- package/dist/threads/api.d.ts +15 -0
- package/dist/threads/api.js.map +1 -1
- package/dist/threads/conversation-list-fetcher.cjs +13 -5
- package/dist/threads/conversation-list-fetcher.cjs.map +1 -1
- package/dist/threads/conversation-list-fetcher.d.ts +4 -1
- package/dist/threads/conversation-list-fetcher.js +13 -5
- package/dist/threads/conversation-list-fetcher.js.map +1 -1
- package/dist/threads/statewire-thread-list.cjs +28 -6
- package/dist/threads/statewire-thread-list.cjs.map +1 -1
- package/dist/threads/statewire-thread-list.d.ts +20 -8
- package/dist/threads/statewire-thread-list.js +28 -6
- package/dist/threads/statewire-thread-list.js.map +1 -1
- package/dist/threads/use-athena-sessions.cjs +27 -5
- package/dist/threads/use-athena-sessions.cjs.map +1 -1
- package/dist/threads/use-athena-sessions.d.ts +10 -5
- package/dist/threads/use-athena-sessions.js +28 -6
- package/dist/threads/use-athena-sessions.js.map +1 -1
- package/dist/threads/use-rest-conversation-history.d.ts +1 -0
- package/dist/tools/write-todos-tool-ui.cjs +8 -0
- package/dist/tools/write-todos-tool-ui.cjs.map +1 -1
- package/dist/tools/write-todos-tool-ui.d.ts +2 -0
- package/dist/tools/write-todos-tool-ui.js +8 -0
- package/dist/tools/write-todos-tool-ui.js.map +1 -1
- package/dist/workbench/session-workbench-panes.cjs +1 -8
- package/dist/workbench/session-workbench-panes.cjs.map +1 -1
- package/dist/workbench/session-workbench-panes.js +2 -9
- package/dist/workbench/session-workbench-panes.js.map +1 -1
- package/package.json +2 -2
|
@@ -51,21 +51,26 @@ type IndexEntryLike = Parameters<typeof entryCouldMatch>[0] & {
|
|
|
51
51
|
thread_id: string;
|
|
52
52
|
updated_at: string;
|
|
53
53
|
app_id: string | null;
|
|
54
|
+
collab_agent_id?: string | null;
|
|
55
|
+
start_channel?: string | null;
|
|
54
56
|
};
|
|
55
57
|
/**
|
|
56
58
|
* The index entries that warrant one refresh of a filtered list, as
|
|
57
59
|
* `<thread_id>@<updated_at>` versions: entries the list does not show, of
|
|
58
|
-
* the apps it lists,
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
60
|
+
* the apps it lists (and, while `homeAgentApplied`, of the conversations with
|
|
61
|
+
* `homeAgentId`'s agent; see `indexedThreadsToRefreshFor`), that the query
|
|
62
|
+
* could match on what the entry carries, and whose version has not prompted a
|
|
63
|
+
* refresh before. An unchanged entry the server keeps leaving out refreshes
|
|
64
|
+
* once; any later change to it refreshes again. Exported for tests.
|
|
62
65
|
*/
|
|
63
|
-
export declare function indexEntryVersionsToRefresh({ entries, listedThreadIds, refreshedVersions, appId, additionalAppIds, query, }: {
|
|
66
|
+
export declare function indexEntryVersionsToRefresh({ entries, listedThreadIds, refreshedVersions, appId, additionalAppIds, homeAgentId, homeAgentApplied, query, }: {
|
|
64
67
|
entries: readonly IndexEntryLike[];
|
|
65
68
|
listedThreadIds: ReadonlySet<string>;
|
|
66
69
|
refreshedVersions: ReadonlySet<string>;
|
|
67
70
|
appId: string | undefined;
|
|
68
71
|
additionalAppIds: readonly string[];
|
|
72
|
+
homeAgentId?: string;
|
|
73
|
+
homeAgentApplied?: boolean;
|
|
69
74
|
query: AthenaSessionsQuery;
|
|
70
75
|
}): string[];
|
|
71
76
|
/**
|
|
@@ -9,7 +9,7 @@ import { sessionListShareKey } from "./session-list-tab-share.js";
|
|
|
9
9
|
import { supportedFiltersFromResponse, sessionsQueryKey, planSessionsQuery, sessionsQueryFromKey, unavailableSessionsQueryFields, matchesSessionsQuery, sortSessions } from "./sessions-query.js";
|
|
10
10
|
import { AthenaSessionsLiveContext, foldConversationIndex, threadSummaryFromConversation, indexedThreadsToRefreshFor } from "./statewire-thread-list.js";
|
|
11
11
|
import { useConversationList } from "../agent-runtime/src/use-conversation-list.js";
|
|
12
|
-
import {
|
|
12
|
+
import { conversationListFilterKey, conversationListQueryKeys } from "../agent-runtime/src/conversation-list.js";
|
|
13
13
|
const DEFAULT_PAGE_SIZE = 50;
|
|
14
14
|
const THREAD_LIST_CAPABILITIES_QUERY_KEY = [
|
|
15
15
|
"athena-sdk",
|
|
@@ -93,14 +93,15 @@ function useAthenaSessions({
|
|
|
93
93
|
enabled: active && capabilitiesSettled,
|
|
94
94
|
queryClient
|
|
95
95
|
});
|
|
96
|
+
const standInScope = `${identityScope}::${conversationListFilterKey(baseFilter)}`;
|
|
96
97
|
const lastResolvedRef = useRef({
|
|
97
98
|
scope: "",
|
|
98
99
|
conversations: []
|
|
99
100
|
});
|
|
100
101
|
if (list.hasResolvedFirstPage) {
|
|
101
|
-
lastResolvedRef.current = { scope:
|
|
102
|
+
lastResolvedRef.current = { scope: standInScope, conversations: list.conversations };
|
|
102
103
|
}
|
|
103
|
-
const standIn = !list.hasResolvedFirstPage && lastResolvedRef.current.scope ===
|
|
104
|
+
const standIn = !list.hasResolvedFirstPage && lastResolvedRef.current.scope === standInScope ? lastResolvedRef.current.conversations : null;
|
|
104
105
|
const source = standIn ?? list.conversations;
|
|
105
106
|
const isPlaceholderData = standIn !== null && standIn.length > 0;
|
|
106
107
|
const indexByThread = live == null ? void 0 : live.indexByThread;
|
|
@@ -116,6 +117,8 @@ function useAthenaSessions({
|
|
|
116
117
|
const listRefresh = list.refresh;
|
|
117
118
|
const baseAppId = baseFilter.appId;
|
|
118
119
|
const baseAppIds = baseFilter.appIds;
|
|
120
|
+
const baseHomeAgentId = baseFilter.homeAgentId;
|
|
121
|
+
const homeAgentApplied = list.homeAgentApplied;
|
|
119
122
|
const listedThreadIds = useMemo(
|
|
120
123
|
() => new Set(list.conversations.map((conversation) => conversation.threadId)),
|
|
121
124
|
[list.conversations]
|
|
@@ -127,9 +130,19 @@ function useAthenaSessions({
|
|
|
127
130
|
refreshedVersions: EMPTY_VERSIONS,
|
|
128
131
|
appId: baseAppId,
|
|
129
132
|
additionalAppIds: baseAppIds ?? [],
|
|
133
|
+
homeAgentId: baseHomeAgentId,
|
|
134
|
+
homeAgentApplied,
|
|
130
135
|
query: plan.local
|
|
131
136
|
}),
|
|
132
|
-
[
|
|
137
|
+
[
|
|
138
|
+
indexEntries,
|
|
139
|
+
listedThreadIds,
|
|
140
|
+
baseAppId,
|
|
141
|
+
baseAppIds,
|
|
142
|
+
baseHomeAgentId,
|
|
143
|
+
homeAgentApplied,
|
|
144
|
+
plan.local
|
|
145
|
+
]
|
|
133
146
|
);
|
|
134
147
|
useIndexTriggeredRefresh({
|
|
135
148
|
pending: unlistedVersions,
|
|
@@ -171,18 +184,27 @@ function indexEntryVersionsToRefresh({
|
|
|
171
184
|
refreshedVersions,
|
|
172
185
|
appId,
|
|
173
186
|
additionalAppIds,
|
|
187
|
+
homeAgentId,
|
|
188
|
+
homeAgentApplied,
|
|
174
189
|
query
|
|
175
190
|
}) {
|
|
176
191
|
const versionOf = (entry) => `${entry.thread_id}@${entry.updated_at}`;
|
|
177
192
|
const byVersion = new Map(entries.map((entry) => [versionOf(entry), entry]));
|
|
178
193
|
return indexedThreadsToRefreshFor({
|
|
179
|
-
entries: entries.map((entry) => ({
|
|
194
|
+
entries: entries.map((entry) => ({
|
|
195
|
+
thread_id: versionOf(entry),
|
|
196
|
+
app_id: entry.app_id,
|
|
197
|
+
collab_agent_id: entry.collab_agent_id,
|
|
198
|
+
start_channel: entry.start_channel
|
|
199
|
+
})),
|
|
180
200
|
listedThreadIds: new Set(
|
|
181
201
|
entries.filter((entry) => listedThreadIds.has(entry.thread_id)).map(versionOf)
|
|
182
202
|
),
|
|
183
203
|
refreshedThreadIds: refreshedVersions,
|
|
184
204
|
appId,
|
|
185
|
-
additionalAppIds
|
|
205
|
+
additionalAppIds,
|
|
206
|
+
homeAgentId,
|
|
207
|
+
homeAgentApplied
|
|
186
208
|
}).filter((version) => {
|
|
187
209
|
const entry = byVersion.get(version);
|
|
188
210
|
return entry === void 0 || entryCouldMatch(entry, query);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-athena-sessions.js","sources":["../../src/threads/use-athena-sessions.ts"],"sourcesContent":["/**\n * `useAthenaSessions(query)` — one filtered, sorted, live session list under\n * `<AthenaProvider enableThreadList>`: the data layer of `SessionsPage` and\n * `Inbox`, for hosts drawing their own.\n *\n * - **Server-side.** The query travels to `POST /api/conversations/threads/list`\n * as filters of the server's one page statement (`planSessionsQuery`), over\n * the shared two-tier pager (`useConversationList`: a live first page and\n * frozen history pages) in the thread-list provider's query client.\n * - **Live.** Rows fold the same conversation-index pushes the sidebar folds\n * (titles, lifecycle, attention flip in place), carry the provider's\n * in-flight writes (pin, rename, read state, archive), and a thread the\n * index learns about that the list does not show yet refreshes the list\n * once.\n * - **Degraded, not wrong.** A server that cannot apply part of the query is\n * detected from its `supported_filters` echo (one `limit: 1` probe per\n * identity). Fields the loaded rows can be tested on are applied to them\n * (`degradedFields`: exact per page, a page can come back short); fields\n * they cannot are reported in `unavailableFields` and not applied, so a\n * control for them can be disabled rather than lie.\n */\nimport {\n type ConversationListFilter,\n type ConversationSummary,\n conversationListFilterKey,\n conversationListQueryKeys,\n useConversationList,\n} from '@athenaintel/agent-runtime';\nimport { useQuery } from '@tanstack/react-query';\nimport { useContext, useMemo, useRef } from 'react';\nimport { useAthenaQueryClient } from '../lib/use-athena-rest-query';\nimport { withCredentialRenewal } from '../runtime/credential-renewal';\nimport { listThreads } from './api';\nimport { createRestConversationListFetcher } from './conversation-list-fetcher';\nimport { useIndexTriggeredRefresh } from './index-triggered-refresh';\nimport { sessionListShareKey } from './session-list-tab-share';\nimport {\n type AthenaSessionsQuery,\n type AthenaSessionsQueryField,\n matchesSessionsQuery,\n planSessionsQuery,\n sessionsQueryFromKey,\n sessionsQueryKey,\n sortSessions,\n supportedFiltersFromResponse,\n unavailableSessionsQueryFields,\n} from './sessions-query';\nimport {\n AthenaSessionsLiveContext,\n type AthenaStatewireThreadSummary,\n foldConversationIndex,\n indexedThreadsToRefreshFor,\n threadSummaryFromConversation,\n} from './statewire-thread-list';\n\nexport interface UseAthenaSessionsOptions {\n /** What to list. A new object with the same content does not refetch. */\n query: AthenaSessionsQuery;\n /** Rows per page (default 50). */\n pageSize?: number;\n /** `false` holds the list idle. Default `true`. */\n enabled?: boolean;\n}\n\nexport interface AthenaSessionsList {\n /** The rows, in the query's order, with live pushes and in-flight writes applied. */\n rows: AthenaStatewireThreadSummary[];\n /** `false` outside `enableThreadList` mode: nothing is fetched and `rows` stays empty. */\n isAvailable: boolean;\n /** True until the first page has answered (or while no credential has resolved). */\n isLoading: boolean;\n isFetching: boolean;\n hasResolvedFirstPage: boolean;\n /**\n * True while the rows are the previous query's (checked against this\n * query's local part) because this query's first page is still in flight.\n */\n isPlaceholderData: boolean;\n hasNextPage: boolean;\n isFetchingNextPage: boolean;\n fetchNextPage: () => void;\n /** Refetch the first page; resolves once it has answered. */\n refresh: () => Promise<void>;\n /** True while the last \"load more\" request failed. */\n isFetchNextPageError: boolean;\n /**\n * The server fields this backend applies (`supported_filters`), `null`\n * while unknown or on a backend that predates the list.\n */\n supportedFilters: ReadonlySet<string> | null;\n /** Fields applied to the loaded rows because the backend cannot. */\n degradedFields: AthenaSessionsQueryField[];\n /** Fields of this query that cannot be applied on this backend at all (and are not). */\n unavailableFields: AthenaSessionsQueryField[];\n /**\n * Every field this backend cannot apply anywhere, selected or not — what a\n * filter UI disables. Empty until the capabilities probe has answered.\n */\n unsupportedFields: AthenaSessionsQueryField[];\n}\n\nconst DEFAULT_PAGE_SIZE = 50;\n\n/** The capabilities probe's key prefix. */\nexport const THREAD_LIST_CAPABILITIES_QUERY_KEY = [\n 'athena-sdk',\n 'thread-list-capabilities',\n] as const;\n\nconst EMPTY_ROWS: AthenaStatewireThreadSummary[] = [];\nconst EMPTY_VERSIONS: ReadonlySet<string> = new Set();\nconst NOOP = () => undefined;\nconst NOOP_ASYNC = async () => undefined;\n\nexport function useAthenaSessions({\n query,\n pageSize = DEFAULT_PAGE_SIZE,\n enabled = true,\n}: UseAthenaSessionsOptions): AthenaSessionsList {\n const live = useContext(AthenaSessionsLiveContext);\n const isAvailable = live !== null;\n const backendUrl = live?.backendUrl ?? '';\n const principal = live?.principal ?? '';\n const hasAuth = live?.hasAuth ?? false;\n const includePullRequests = live?.includePullRequests ?? false;\n const getAuth = live?.getAuth;\n const active = isAvailable && enabled && hasAuth;\n // Every cache below is keyed by WHO reads (principal) and WITH WHAT (the\n // credential generation): a scoped credential for another workspace under\n // the same principal must never read this one's cached rows.\n const identityScope = `${live?.scopeKey ?? 'unavailable'}::credential-${\n live?.credentialGeneration ?? 0\n }`;\n\n // Outside thread-list mode there may be no `QueryClientProvider`; every\n // query below is disabled there, on a client of this hook's own.\n const queryClient = useAthenaQueryClient();\n\n // Which filters this backend applies: one tiny page per identity. The\n // answer is a property of the deployed agora, so it never goes stale.\n const capabilities = useQuery(\n {\n queryKey: [\n ...THREAD_LIST_CAPABILITIES_QUERY_KEY,\n backendUrl,\n principal,\n live?.credentialGeneration ?? 0,\n ],\n queryFn: async ({ signal }) => {\n if (!getAuth) return null;\n const response = await withCredentialRenewal(getAuth, (auth) =>\n listThreads(backendUrl, auth, { limit: 1, signal }),\n );\n return supportedFiltersFromResponse(response.supported_filters);\n },\n enabled: active,\n staleTime: Number.POSITIVE_INFINITY,\n retry: 1,\n },\n queryClient,\n );\n // A failed probe is treated as an older backend: the query degrades\n // instead of the list staying empty.\n const capabilitiesSettled = capabilities.isSuccess || capabilities.isError;\n const supportedFilters = capabilities.data ?? null;\n\n // `query` may be a fresh object every render; its normalized key is what\n // identifies it, so the plan is built from the key and recomputed only\n // when the content changes.\n const queryKey = sessionsQueryKey(query);\n const plan = useMemo(\n () =>\n planSessionsQuery(sessionsQueryFromKey(queryKey), {\n supportedFilters,\n includePullRequests,\n }),\n [queryKey, supportedFilters, includePullRequests],\n );\n const planRef = useRef(plan);\n planRef.current = plan;\n const unsupportedFields = useMemo(\n () =>\n capabilitiesSettled\n ? unavailableSessionsQueryFields({ supportedFilters, includePullRequests })\n : [],\n [capabilitiesSettled, supportedFilters, includePullRequests],\n );\n\n const fetchPage = useMemo(\n () =>\n getAuth\n ? createRestConversationListFetcher({\n backendUrl,\n getAuth,\n includePullRequests,\n extraFilters: plan.body,\n keepRow: (row) => matchesSessionsQuery(row, planRef.current.local),\n })\n : async () => ({ items: [], hasMore: false, totalCount: null }),\n [backendUrl, getAuth, includePullRequests, plan.body],\n );\n const baseFilter: ConversationListFilter = live?.baseFilter ?? {};\n const capabilityKey = supportedFilters ? [...supportedFilters].sort().join(',') : 'legacy';\n // The query's server filters are not part of the shared filter contract,\n // so they — and what this backend applies of them — key the cache here.\n const listScopeKey = `${identityScope}::sessions::${queryKey}::${capabilityKey}`;\n const list = useConversationList({\n fetchPage,\n filter: baseFilter,\n pageSize,\n scopeKey: listScopeKey,\n enabled: active && capabilitiesSettled,\n queryClient,\n });\n\n // While a changed query's first page is in flight, the previous query's\n // rows stand in, checked against the new query's local part — the list\n // narrows at once instead of blanking to skeletons. Only rows read as the\n // same identity and credential ever stand in.\n const lastResolvedRef = useRef<{ scope: string; conversations: ConversationSummary[] }>({\n scope: '',\n conversations: [],\n });\n if (list.hasResolvedFirstPage) {\n lastResolvedRef.current = { scope: identityScope, conversations: list.conversations };\n }\n const standIn =\n !list.hasResolvedFirstPage && lastResolvedRef.current.scope === identityScope\n ? lastResolvedRef.current.conversations\n : null;\n const source = standIn ?? list.conversations;\n const isPlaceholderData = standIn !== null && standIn.length > 0;\n\n const indexByThread = live?.indexByThread;\n const applyOverrides = live?.applyOverrides;\n const rows = useMemo(() => {\n if (!indexByThread || !applyOverrides) return EMPTY_ROWS;\n const folded: ConversationSummary[] = applyOverrides(\n foldConversationIndex(source, indexByThread),\n ).filter((row) => matchesSessionsQuery(row, plan.local));\n return sortSessions(folded, plan.local).map(threadSummaryFromConversation);\n }, [source, indexByThread, applyOverrides, plan.local]);\n\n // A thread the index knows that this list does not show yet refreshes the\n // list — it may be new, or may have just come to match. Once per VERSION of\n // the entry (its id and `updated_at`): an unchanged entry the server keeps\n // leaving out never refreshes twice, and a later change to it (a PR opened,\n // a run now needing input) gets its own refresh. Coalesced, held while the\n // list is already being read, and a refused refresh is tried again\n // (`useIndexTriggeredRefresh`).\n const indexEntries = live?.indexEntries ?? null;\n const listRefresh = list.refresh;\n const baseAppId = baseFilter.appId;\n const baseAppIds = baseFilter.appIds;\n const listedThreadIds = useMemo(\n () => new Set(list.conversations.map((conversation) => conversation.threadId)),\n [list.conversations],\n );\n const unlistedVersions = useMemo(\n () =>\n indexEntries === null\n ? []\n : indexEntryVersionsToRefresh({\n entries: indexEntries,\n listedThreadIds,\n refreshedVersions: EMPTY_VERSIONS,\n appId: baseAppId,\n additionalAppIds: baseAppIds ?? [],\n query: plan.local,\n }),\n [indexEntries, listedThreadIds, baseAppId, baseAppIds, plan.local],\n );\n useIndexTriggeredRefresh({\n pending: unlistedVersions,\n refresh: listRefresh,\n // Held while the list is being read; a refused first page does not hold\n // it (the refresh is how the list recovers).\n enabled: active && capabilitiesSettled && indexEntries !== null && !list.isFetching,\n tabShare: live?.tabShare,\n shareKey: sessionListShareKey(\n conversationListQueryKeys.firstPage(\n listScopeKey,\n conversationListFilterKey(baseFilter),\n pageSize,\n ),\n ),\n });\n\n const listFetchNextPage = list.fetchNextPage;\n return {\n rows: isAvailable ? rows : EMPTY_ROWS,\n isAvailable,\n isLoading: active && (!capabilitiesSettled || list.isLoading),\n isFetching: list.isFetching || capabilities.isFetching,\n hasResolvedFirstPage: list.hasResolvedFirstPage,\n isPlaceholderData,\n hasNextPage: list.hasNextPage,\n isFetchingNextPage: list.isFetchingNextPage,\n fetchNextPage: isAvailable ? () => void listFetchNextPage() : NOOP,\n refresh: isAvailable ? async () => void (await listRefresh()) : NOOP_ASYNC,\n isFetchNextPageError: list.isFetchNextPageError,\n supportedFilters,\n degradedFields: plan.degraded,\n unavailableFields: plan.unavailable,\n unsupportedFields,\n };\n}\n\ntype IndexEntryLike = Parameters<typeof entryCouldMatch>[0] & {\n thread_id: string;\n updated_at: string;\n app_id: string | null;\n};\n\n/**\n * The index entries that warrant one refresh of a filtered list, as\n * `<thread_id>@<updated_at>` versions: entries the list does not show, of\n * the apps it lists, that the query could match on what the entry carries,\n * and whose version has not prompted a refresh before. An unchanged entry the\n * server keeps leaving out refreshes once; any later change to it refreshes\n * again. Exported for tests.\n */\nexport function indexEntryVersionsToRefresh({\n entries,\n listedThreadIds,\n refreshedVersions,\n appId,\n additionalAppIds,\n query,\n}: {\n entries: readonly IndexEntryLike[];\n listedThreadIds: ReadonlySet<string>;\n refreshedVersions: ReadonlySet<string>;\n appId: string | undefined;\n additionalAppIds: readonly string[];\n query: AthenaSessionsQuery;\n}): string[] {\n const versionOf = (entry: IndexEntryLike) => `${entry.thread_id}@${entry.updated_at}`;\n const byVersion = new Map(entries.map((entry) => [versionOf(entry), entry]));\n return indexedThreadsToRefreshFor({\n entries: entries.map((entry) => ({ thread_id: versionOf(entry), app_id: entry.app_id })),\n listedThreadIds: new Set(\n entries.filter((entry) => listedThreadIds.has(entry.thread_id)).map(versionOf),\n ),\n refreshedThreadIds: refreshedVersions,\n appId,\n additionalAppIds,\n }).filter((version) => {\n const entry = byVersion.get(version);\n return entry === undefined || entryCouldMatch(entry, query);\n });\n}\n\n/**\n * Whether a pushed index entry could belong to a list with this local query:\n * the fields the entry carries are checked, the rest pass.\n */\nexport function entryCouldMatch(\n entry: {\n is_unread?: boolean | null;\n is_favorited?: boolean | null;\n run_status?: string | null;\n session_status?: string | null;\n },\n query: AthenaSessionsQuery,\n): boolean {\n if (query.unreadOnly && entry.is_unread === false) return false;\n if (query.favoritesOnly && entry.is_favorited === false) return false;\n if (\n query.runStatuses &&\n typeof entry.run_status === 'string' &&\n !(query.runStatuses as readonly string[]).includes(entry.run_status)\n ) {\n return false;\n }\n if (\n query.sessionStatuses &&\n typeof entry.session_status === 'string' &&\n !(query.sessionStatuses as readonly string[]).includes(entry.session_status)\n ) {\n return false;\n }\n return true;\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-athena-sessions.js","sources":["../../src/threads/use-athena-sessions.ts"],"sourcesContent":["/**\n * `useAthenaSessions(query)` — one filtered, sorted, live session list under\n * `<AthenaProvider enableThreadList>`: the data layer of `SessionsPage` and\n * `Inbox`, for hosts drawing their own.\n *\n * - **Server-side.** The query travels to `POST /api/conversations/threads/list`\n * as filters of the server's one page statement (`planSessionsQuery`), over\n * the shared two-tier pager (`useConversationList`: a live first page and\n * frozen history pages) in the thread-list provider's query client.\n * - **Live.** Rows fold the same conversation-index pushes the sidebar folds\n * (titles, lifecycle, attention flip in place), carry the provider's\n * in-flight writes (pin, rename, read state, archive), and a thread the\n * index learns about that the list does not show yet refreshes the list\n * once.\n * - **Degraded, not wrong.** A server that cannot apply part of the query is\n * detected from its `supported_filters` echo (one `limit: 1` probe per\n * identity). Fields the loaded rows can be tested on are applied to them\n * (`degradedFields`: exact per page, a page can come back short); fields\n * they cannot are reported in `unavailableFields` and not applied, so a\n * control for them can be disabled rather than lie.\n */\nimport {\n type ConversationListFilter,\n type ConversationSummary,\n conversationListFilterKey,\n conversationListQueryKeys,\n useConversationList,\n} from '@athenaintel/agent-runtime';\nimport { useQuery } from '@tanstack/react-query';\nimport { useContext, useMemo, useRef } from 'react';\nimport { useAthenaQueryClient } from '../lib/use-athena-rest-query';\nimport { withCredentialRenewal } from '../runtime/credential-renewal';\nimport { listThreads } from './api';\nimport { createRestConversationListFetcher } from './conversation-list-fetcher';\nimport { useIndexTriggeredRefresh } from './index-triggered-refresh';\nimport { sessionListShareKey } from './session-list-tab-share';\nimport {\n type AthenaSessionsQuery,\n type AthenaSessionsQueryField,\n matchesSessionsQuery,\n planSessionsQuery,\n sessionsQueryFromKey,\n sessionsQueryKey,\n sortSessions,\n supportedFiltersFromResponse,\n unavailableSessionsQueryFields,\n} from './sessions-query';\nimport {\n AthenaSessionsLiveContext,\n type AthenaStatewireThreadSummary,\n foldConversationIndex,\n indexedThreadsToRefreshFor,\n threadSummaryFromConversation,\n} from './statewire-thread-list';\n\nexport interface UseAthenaSessionsOptions {\n /** What to list. A new object with the same content does not refetch. */\n query: AthenaSessionsQuery;\n /** Rows per page (default 50). */\n pageSize?: number;\n /** `false` holds the list idle. Default `true`. */\n enabled?: boolean;\n}\n\nexport interface AthenaSessionsList {\n /** The rows, in the query's order, with live pushes and in-flight writes applied. */\n rows: AthenaStatewireThreadSummary[];\n /** `false` outside `enableThreadList` mode: nothing is fetched and `rows` stays empty. */\n isAvailable: boolean;\n /** True until the first page has answered (or while no credential has resolved). */\n isLoading: boolean;\n isFetching: boolean;\n hasResolvedFirstPage: boolean;\n /**\n * True while the rows are the previous query's (checked against this\n * query's local part) because this query's first page is still in flight.\n */\n isPlaceholderData: boolean;\n hasNextPage: boolean;\n isFetchingNextPage: boolean;\n fetchNextPage: () => void;\n /** Refetch the first page; resolves once it has answered. */\n refresh: () => Promise<void>;\n /** True while the last \"load more\" request failed. */\n isFetchNextPageError: boolean;\n /**\n * The server fields this backend applies (`supported_filters`), `null`\n * while unknown or on a backend that predates the list.\n */\n supportedFilters: ReadonlySet<string> | null;\n /** Fields applied to the loaded rows because the backend cannot. */\n degradedFields: AthenaSessionsQueryField[];\n /** Fields of this query that cannot be applied on this backend at all (and are not). */\n unavailableFields: AthenaSessionsQueryField[];\n /**\n * Every field this backend cannot apply anywhere, selected or not — what a\n * filter UI disables. Empty until the capabilities probe has answered.\n */\n unsupportedFields: AthenaSessionsQueryField[];\n}\n\nconst DEFAULT_PAGE_SIZE = 50;\n\n/** The capabilities probe's key prefix. */\nexport const THREAD_LIST_CAPABILITIES_QUERY_KEY = [\n 'athena-sdk',\n 'thread-list-capabilities',\n] as const;\n\nconst EMPTY_ROWS: AthenaStatewireThreadSummary[] = [];\nconst EMPTY_VERSIONS: ReadonlySet<string> = new Set();\nconst NOOP = () => undefined;\nconst NOOP_ASYNC = async () => undefined;\n\nexport function useAthenaSessions({\n query,\n pageSize = DEFAULT_PAGE_SIZE,\n enabled = true,\n}: UseAthenaSessionsOptions): AthenaSessionsList {\n const live = useContext(AthenaSessionsLiveContext);\n const isAvailable = live !== null;\n const backendUrl = live?.backendUrl ?? '';\n const principal = live?.principal ?? '';\n const hasAuth = live?.hasAuth ?? false;\n const includePullRequests = live?.includePullRequests ?? false;\n const getAuth = live?.getAuth;\n const active = isAvailable && enabled && hasAuth;\n // Every cache below is keyed by WHO reads (principal) and WITH WHAT (the\n // credential generation): a scoped credential for another workspace under\n // the same principal must never read this one's cached rows.\n const identityScope = `${live?.scopeKey ?? 'unavailable'}::credential-${\n live?.credentialGeneration ?? 0\n }`;\n\n // Outside thread-list mode there may be no `QueryClientProvider`; every\n // query below is disabled there, on a client of this hook's own.\n const queryClient = useAthenaQueryClient();\n\n // Which filters this backend applies: one tiny page per identity. The\n // answer is a property of the deployed agora, so it never goes stale.\n const capabilities = useQuery(\n {\n queryKey: [\n ...THREAD_LIST_CAPABILITIES_QUERY_KEY,\n backendUrl,\n principal,\n live?.credentialGeneration ?? 0,\n ],\n queryFn: async ({ signal }) => {\n if (!getAuth) return null;\n const response = await withCredentialRenewal(getAuth, (auth) =>\n listThreads(backendUrl, auth, { limit: 1, signal }),\n );\n return supportedFiltersFromResponse(response.supported_filters);\n },\n enabled: active,\n staleTime: Number.POSITIVE_INFINITY,\n retry: 1,\n },\n queryClient,\n );\n // A failed probe is treated as an older backend: the query degrades\n // instead of the list staying empty.\n const capabilitiesSettled = capabilities.isSuccess || capabilities.isError;\n const supportedFilters = capabilities.data ?? null;\n\n // `query` may be a fresh object every render; its normalized key is what\n // identifies it, so the plan is built from the key and recomputed only\n // when the content changes.\n const queryKey = sessionsQueryKey(query);\n const plan = useMemo(\n () =>\n planSessionsQuery(sessionsQueryFromKey(queryKey), {\n supportedFilters,\n includePullRequests,\n }),\n [queryKey, supportedFilters, includePullRequests],\n );\n const planRef = useRef(plan);\n planRef.current = plan;\n const unsupportedFields = useMemo(\n () =>\n capabilitiesSettled\n ? unavailableSessionsQueryFields({ supportedFilters, includePullRequests })\n : [],\n [capabilitiesSettled, supportedFilters, includePullRequests],\n );\n\n const fetchPage = useMemo(\n () =>\n getAuth\n ? createRestConversationListFetcher({\n backendUrl,\n getAuth,\n includePullRequests,\n extraFilters: plan.body,\n keepRow: (row) => matchesSessionsQuery(row, planRef.current.local),\n })\n : async () => ({ items: [], hasMore: false, totalCount: null }),\n [backendUrl, getAuth, includePullRequests, plan.body],\n );\n const baseFilter: ConversationListFilter = live?.baseFilter ?? {};\n const capabilityKey = supportedFilters ? [...supportedFilters].sort().join(',') : 'legacy';\n // The query's server filters are not part of the shared filter contract,\n // so they — and what this backend applies of them — key the cache here.\n const listScopeKey = `${identityScope}::sessions::${queryKey}::${capabilityKey}`;\n const list = useConversationList({\n fetchPage,\n filter: baseFilter,\n pageSize,\n scopeKey: listScopeKey,\n enabled: active && capabilitiesSettled,\n queryClient,\n });\n\n // While a changed query's first page is in flight, the previous query's\n // rows stand in, checked against the new query's local part — the list\n // narrows at once instead of blanking to skeletons. Only rows read as the\n // same identity and credential, over the same app scope, ever stand in: a\n // changed scope (another app, or the home agent's conversations turned off)\n // cannot be narrowed on the rows.\n const standInScope = `${identityScope}::${conversationListFilterKey(baseFilter)}`;\n const lastResolvedRef = useRef<{ scope: string; conversations: ConversationSummary[] }>({\n scope: '',\n conversations: [],\n });\n if (list.hasResolvedFirstPage) {\n lastResolvedRef.current = { scope: standInScope, conversations: list.conversations };\n }\n const standIn =\n !list.hasResolvedFirstPage && lastResolvedRef.current.scope === standInScope\n ? lastResolvedRef.current.conversations\n : null;\n const source = standIn ?? list.conversations;\n const isPlaceholderData = standIn !== null && standIn.length > 0;\n\n const indexByThread = live?.indexByThread;\n const applyOverrides = live?.applyOverrides;\n const rows = useMemo(() => {\n if (!indexByThread || !applyOverrides) return EMPTY_ROWS;\n const folded: ConversationSummary[] = applyOverrides(\n foldConversationIndex(source, indexByThread),\n ).filter((row) => matchesSessionsQuery(row, plan.local));\n return sortSessions(folded, plan.local).map(threadSummaryFromConversation);\n }, [source, indexByThread, applyOverrides, plan.local]);\n\n // A thread the index knows that this list does not show yet refreshes the\n // list — it may be new, or may have just come to match. Once per VERSION of\n // the entry (its id and `updated_at`): an unchanged entry the server keeps\n // leaving out never refreshes twice, and a later change to it (a PR opened,\n // a run now needing input) gets its own refresh. Coalesced, held while the\n // list is already being read, and a refused refresh is tried again\n // (`useIndexTriggeredRefresh`).\n const indexEntries = live?.indexEntries ?? null;\n const listRefresh = list.refresh;\n const baseAppId = baseFilter.appId;\n const baseAppIds = baseFilter.appIds;\n const baseHomeAgentId = baseFilter.homeAgentId;\n const homeAgentApplied = list.homeAgentApplied;\n const listedThreadIds = useMemo(\n () => new Set(list.conversations.map((conversation) => conversation.threadId)),\n [list.conversations],\n );\n const unlistedVersions = useMemo(\n () =>\n indexEntries === null\n ? []\n : indexEntryVersionsToRefresh({\n entries: indexEntries,\n listedThreadIds,\n refreshedVersions: EMPTY_VERSIONS,\n appId: baseAppId,\n additionalAppIds: baseAppIds ?? [],\n homeAgentId: baseHomeAgentId,\n homeAgentApplied,\n query: plan.local,\n }),\n [\n indexEntries,\n listedThreadIds,\n baseAppId,\n baseAppIds,\n baseHomeAgentId,\n homeAgentApplied,\n plan.local,\n ],\n );\n useIndexTriggeredRefresh({\n pending: unlistedVersions,\n refresh: listRefresh,\n // Held while the list is being read; a refused first page does not hold\n // it (the refresh is how the list recovers).\n enabled: active && capabilitiesSettled && indexEntries !== null && !list.isFetching,\n tabShare: live?.tabShare,\n shareKey: sessionListShareKey(\n conversationListQueryKeys.firstPage(\n listScopeKey,\n conversationListFilterKey(baseFilter),\n pageSize,\n ),\n ),\n });\n\n const listFetchNextPage = list.fetchNextPage;\n return {\n rows: isAvailable ? rows : EMPTY_ROWS,\n isAvailable,\n isLoading: active && (!capabilitiesSettled || list.isLoading),\n isFetching: list.isFetching || capabilities.isFetching,\n hasResolvedFirstPage: list.hasResolvedFirstPage,\n isPlaceholderData,\n hasNextPage: list.hasNextPage,\n isFetchingNextPage: list.isFetchingNextPage,\n fetchNextPage: isAvailable ? () => void listFetchNextPage() : NOOP,\n refresh: isAvailable ? async () => void (await listRefresh()) : NOOP_ASYNC,\n isFetchNextPageError: list.isFetchNextPageError,\n supportedFilters,\n degradedFields: plan.degraded,\n unavailableFields: plan.unavailable,\n unsupportedFields,\n };\n}\n\ntype IndexEntryLike = Parameters<typeof entryCouldMatch>[0] & {\n thread_id: string;\n updated_at: string;\n app_id: string | null;\n collab_agent_id?: string | null;\n start_channel?: string | null;\n};\n\n/**\n * The index entries that warrant one refresh of a filtered list, as\n * `<thread_id>@<updated_at>` versions: entries the list does not show, of\n * the apps it lists (and, while `homeAgentApplied`, of the conversations with\n * `homeAgentId`'s agent; see `indexedThreadsToRefreshFor`), that the query\n * could match on what the entry carries, and whose version has not prompted a\n * refresh before. An unchanged entry the server keeps leaving out refreshes\n * once; any later change to it refreshes again. Exported for tests.\n */\nexport function indexEntryVersionsToRefresh({\n entries,\n listedThreadIds,\n refreshedVersions,\n appId,\n additionalAppIds,\n homeAgentId,\n homeAgentApplied,\n query,\n}: {\n entries: readonly IndexEntryLike[];\n listedThreadIds: ReadonlySet<string>;\n refreshedVersions: ReadonlySet<string>;\n appId: string | undefined;\n additionalAppIds: readonly string[];\n homeAgentId?: string;\n homeAgentApplied?: boolean;\n query: AthenaSessionsQuery;\n}): string[] {\n const versionOf = (entry: IndexEntryLike) => `${entry.thread_id}@${entry.updated_at}`;\n const byVersion = new Map(entries.map((entry) => [versionOf(entry), entry]));\n return indexedThreadsToRefreshFor({\n entries: entries.map((entry) => ({\n thread_id: versionOf(entry),\n app_id: entry.app_id,\n collab_agent_id: entry.collab_agent_id,\n start_channel: entry.start_channel,\n })),\n listedThreadIds: new Set(\n entries.filter((entry) => listedThreadIds.has(entry.thread_id)).map(versionOf),\n ),\n refreshedThreadIds: refreshedVersions,\n appId,\n additionalAppIds,\n homeAgentId,\n homeAgentApplied,\n }).filter((version) => {\n const entry = byVersion.get(version);\n return entry === undefined || entryCouldMatch(entry, query);\n });\n}\n\n/**\n * Whether a pushed index entry could belong to a list with this local query:\n * the fields the entry carries are checked, the rest pass.\n */\nexport function entryCouldMatch(\n entry: {\n is_unread?: boolean | null;\n is_favorited?: boolean | null;\n run_status?: string | null;\n session_status?: string | null;\n },\n query: AthenaSessionsQuery,\n): boolean {\n if (query.unreadOnly && entry.is_unread === false) return false;\n if (query.favoritesOnly && entry.is_favorited === false) return false;\n if (\n query.runStatuses &&\n typeof entry.run_status === 'string' &&\n !(query.runStatuses as readonly string[]).includes(entry.run_status)\n ) {\n return false;\n }\n if (\n query.sessionStatuses &&\n typeof entry.session_status === 'string' &&\n !(query.sessionStatuses as readonly string[]).includes(entry.session_status)\n ) {\n return false;\n }\n return true;\n}\n"],"names":[],"mappings":";;;;;;;;;;;;AAqGA,MAAM,oBAAoB;AAGnB,MAAM,qCAAqC;AAAA,EAChD;AAAA,EACA;AACF;AAEA,MAAM,aAA6C,CAAA;AACnD,MAAM,qCAA0C,IAAA;AAChD,MAAM,OAAO,MAAM;AACnB,MAAM,aAAa,YAAY;AAExB,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA,WAAW;AAAA,EACX,UAAU;AACZ,GAAiD;AAC/C,QAAM,OAAO,WAAW,yBAAyB;AACjD,QAAM,cAAc,SAAS;AAC7B,QAAM,cAAa,6BAAM,eAAc;AACvC,QAAM,aAAY,6BAAM,cAAa;AACrC,QAAM,WAAU,6BAAM,YAAW;AACjC,QAAM,uBAAsB,6BAAM,wBAAuB;AACzD,QAAM,UAAU,6BAAM;AACtB,QAAM,SAAS,eAAe,WAAW;AAIzC,QAAM,gBAAgB,IAAG,6BAAM,aAAY,aAAa,iBACtD,6BAAM,yBAAwB,CAChC;AAIA,QAAM,cAAc,qBAAA;AAIpB,QAAM,eAAe;AAAA,IACnB;AAAA,MACE,UAAU;AAAA,QACR,GAAG;AAAA,QACH;AAAA,QACA;AAAA,SACA,6BAAM,yBAAwB;AAAA,MAAA;AAAA,MAEhC,SAAS,OAAO,EAAE,aAAa;AAC7B,YAAI,CAAC,QAAS,QAAO;AACrB,cAAM,WAAW,MAAM;AAAA,UAAsB;AAAA,UAAS,CAAC,SACrD,YAAY,YAAY,MAAM,EAAE,OAAO,GAAG,OAAA,CAAQ;AAAA,QAAA;AAEpD,eAAO,6BAA6B,SAAS,iBAAiB;AAAA,MAChE;AAAA,MACA,SAAS;AAAA,MACT,WAAW,OAAO;AAAA,MAClB,OAAO;AAAA,IAAA;AAAA,IAET;AAAA,EAAA;AAIF,QAAM,sBAAsB,aAAa,aAAa,aAAa;AACnE,QAAM,mBAAmB,aAAa,QAAQ;AAK9C,QAAM,WAAW,iBAAiB,KAAK;AACvC,QAAM,OAAO;AAAA,IACX,MACE,kBAAkB,qBAAqB,QAAQ,GAAG;AAAA,MAChD;AAAA,MACA;AAAA,IAAA,CACD;AAAA,IACH,CAAC,UAAU,kBAAkB,mBAAmB;AAAA,EAAA;AAElD,QAAM,UAAU,OAAO,IAAI;AAC3B,UAAQ,UAAU;AAClB,QAAM,oBAAoB;AAAA,IACxB,MACE,sBACI,+BAA+B,EAAE,kBAAkB,oBAAA,CAAqB,IACxE,CAAA;AAAA,IACN,CAAC,qBAAqB,kBAAkB,mBAAmB;AAAA,EAAA;AAG7D,QAAM,YAAY;AAAA,IAChB,MACE,UACI,kCAAkC;AAAA,MAChC;AAAA,MACA;AAAA,MACA;AAAA,MACA,cAAc,KAAK;AAAA,MACnB,SAAS,CAAC,QAAQ,qBAAqB,KAAK,QAAQ,QAAQ,KAAK;AAAA,IAAA,CAClE,IACD,aAAa,EAAE,OAAO,CAAA,GAAI,SAAS,OAAO,YAAY;IAC5D,CAAC,YAAY,SAAS,qBAAqB,KAAK,IAAI;AAAA,EAAA;AAEtD,QAAM,cAAqC,6BAAM,eAAc,CAAA;AAC/D,QAAM,gBAAgB,mBAAmB,CAAC,GAAG,gBAAgB,EAAE,OAAO,KAAK,GAAG,IAAI;AAGlF,QAAM,eAAe,GAAG,aAAa,eAAe,QAAQ,KAAK,aAAa;AAC9E,QAAM,OAAO,oBAAoB;AAAA,IAC/B;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,IACA,UAAU;AAAA,IACV,SAAS,UAAU;AAAA,IACnB;AAAA,EAAA,CACD;AAQD,QAAM,eAAe,GAAG,aAAa,KAAK,0BAA0B,UAAU,CAAC;AAC/E,QAAM,kBAAkB,OAAgE;AAAA,IACtF,OAAO;AAAA,IACP,eAAe,CAAA;AAAA,EAAC,CACjB;AACD,MAAI,KAAK,sBAAsB;AAC7B,oBAAgB,UAAU,EAAE,OAAO,cAAc,eAAe,KAAK,cAAA;AAAA,EACvE;AACA,QAAM,UACJ,CAAC,KAAK,wBAAwB,gBAAgB,QAAQ,UAAU,eAC5D,gBAAgB,QAAQ,gBACxB;AACN,QAAM,SAAS,WAAW,KAAK;AAC/B,QAAM,oBAAoB,YAAY,QAAQ,QAAQ,SAAS;AAE/D,QAAM,gBAAgB,6BAAM;AAC5B,QAAM,iBAAiB,6BAAM;AAC7B,QAAM,OAAO,QAAQ,MAAM;AACzB,QAAI,CAAC,iBAAiB,CAAC,eAAgB,QAAO;AAC9C,UAAM,SAAgC;AAAA,MACpC,sBAAsB,QAAQ,aAAa;AAAA,IAAA,EAC3C,OAAO,CAAC,QAAQ,qBAAqB,KAAK,KAAK,KAAK,CAAC;AACvD,WAAO,aAAa,QAAQ,KAAK,KAAK,EAAE,IAAI,6BAA6B;AAAA,EAC3E,GAAG,CAAC,QAAQ,eAAe,gBAAgB,KAAK,KAAK,CAAC;AAStD,QAAM,gBAAe,6BAAM,iBAAgB;AAC3C,QAAM,cAAc,KAAK;AACzB,QAAM,YAAY,WAAW;AAC7B,QAAM,aAAa,WAAW;AAC9B,QAAM,kBAAkB,WAAW;AACnC,QAAM,mBAAmB,KAAK;AAC9B,QAAM,kBAAkB;AAAA,IACtB,MAAM,IAAI,IAAI,KAAK,cAAc,IAAI,CAAC,iBAAiB,aAAa,QAAQ,CAAC;AAAA,IAC7E,CAAC,KAAK,aAAa;AAAA,EAAA;AAErB,QAAM,mBAAmB;AAAA,IACvB,MACE,iBAAiB,OACb,CAAA,IACA,4BAA4B;AAAA,MAC1B,SAAS;AAAA,MACT;AAAA,MACA,mBAAmB;AAAA,MACnB,OAAO;AAAA,MACP,kBAAkB,cAAc,CAAA;AAAA,MAChC,aAAa;AAAA,MACb;AAAA,MACA,OAAO,KAAK;AAAA,IAAA,CACb;AAAA,IACP;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,KAAK;AAAA,IAAA;AAAA,EACP;AAEF,2BAAyB;AAAA,IACvB,SAAS;AAAA,IACT,SAAS;AAAA;AAAA;AAAA,IAGT,SAAS,UAAU,uBAAuB,iBAAiB,QAAQ,CAAC,KAAK;AAAA,IACzE,UAAU,6BAAM;AAAA,IAChB,UAAU;AAAA,MACR,0BAA0B;AAAA,QACxB;AAAA,QACA,0BAA0B,UAAU;AAAA,QACpC;AAAA,MAAA;AAAA,IACF;AAAA,EACF,CACD;AAED,QAAM,oBAAoB,KAAK;AAC/B,SAAO;AAAA,IACL,MAAM,cAAc,OAAO;AAAA,IAC3B;AAAA,IACA,WAAW,WAAW,CAAC,uBAAuB,KAAK;AAAA,IACnD,YAAY,KAAK,cAAc,aAAa;AAAA,IAC5C,sBAAsB,KAAK;AAAA,IAC3B;AAAA,IACA,aAAa,KAAK;AAAA,IAClB,oBAAoB,KAAK;AAAA,IACzB,eAAe,cAAc,MAAM,KAAK,sBAAsB;AAAA,IAC9D,SAAS,cAAc,YAAY,KAAM,MAAM,gBAAiB;AAAA,IAChE,sBAAsB,KAAK;AAAA,IAC3B;AAAA,IACA,gBAAgB,KAAK;AAAA,IACrB,mBAAmB,KAAK;AAAA,IACxB;AAAA,EAAA;AAEJ;AAmBO,SAAS,4BAA4B;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GASa;AACX,QAAM,YAAY,CAAC,UAA0B,GAAG,MAAM,SAAS,IAAI,MAAM,UAAU;AACnF,QAAM,YAAY,IAAI,IAAI,QAAQ,IAAI,CAAC,UAAU,CAAC,UAAU,KAAK,GAAG,KAAK,CAAC,CAAC;AAC3E,SAAO,2BAA2B;AAAA,IAChC,SAAS,QAAQ,IAAI,CAAC,WAAW;AAAA,MAC/B,WAAW,UAAU,KAAK;AAAA,MAC1B,QAAQ,MAAM;AAAA,MACd,iBAAiB,MAAM;AAAA,MACvB,eAAe,MAAM;AAAA,IAAA,EACrB;AAAA,IACF,iBAAiB,IAAI;AAAA,MACnB,QAAQ,OAAO,CAAC,UAAU,gBAAgB,IAAI,MAAM,SAAS,CAAC,EAAE,IAAI,SAAS;AAAA,IAAA;AAAA,IAE/E,oBAAoB;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA,CACD,EAAE,OAAO,CAAC,YAAY;AACrB,UAAM,QAAQ,UAAU,IAAI,OAAO;AACnC,WAAO,UAAU,UAAa,gBAAgB,OAAO,KAAK;AAAA,EAC5D,CAAC;AACH;AAMO,SAAS,gBACd,OAMA,OACS;AACT,MAAI,MAAM,cAAc,MAAM,cAAc,MAAO,QAAO;AAC1D,MAAI,MAAM,iBAAiB,MAAM,iBAAiB,MAAO,QAAO;AAChE,MACE,MAAM,eACN,OAAO,MAAM,eAAe,YAC5B,CAAE,MAAM,YAAkC,SAAS,MAAM,UAAU,GACnE;AACA,WAAO;AAAA,EACT;AACA,MACE,MAAM,mBACN,OAAO,MAAM,mBAAmB,YAChC,CAAE,MAAM,gBAAsC,SAAS,MAAM,cAAc,GAC3E;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;"}
|
|
@@ -21,6 +21,7 @@ export interface UseRestConversationHistoryOptions {
|
|
|
21
21
|
*/
|
|
22
22
|
export declare function useRestConversationHistory({ backendUrl, principalId, getAuth, search, enabled, pageSize, }: UseRestConversationHistoryOptions): {
|
|
23
23
|
totalCount: number | null;
|
|
24
|
+
homeAgentApplied: boolean;
|
|
24
25
|
isLoading: boolean;
|
|
25
26
|
isFetching: boolean;
|
|
26
27
|
hasResolvedFirstPage: boolean;
|
|
@@ -83,6 +83,13 @@ function parseTodoPlan({
|
|
|
83
83
|
changeDescription: asDescription(resultRecord == null ? void 0 : resultRecord.description) ?? asDescription(resultRecord == null ? void 0 : resultRecord.todoChangeDescription)
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
|
+
const PLAN_STEP_STATUS_LABEL = {
|
|
87
|
+
pending: "To do",
|
|
88
|
+
in_progress: "In progress",
|
|
89
|
+
completed: "Done",
|
|
90
|
+
failed: "Failed",
|
|
91
|
+
cancelled: "Cancelled"
|
|
92
|
+
};
|
|
86
93
|
function PlanStepMarker({
|
|
87
94
|
status,
|
|
88
95
|
index
|
|
@@ -265,6 +272,7 @@ const WriteTodosToolUI = require$$0.memo(
|
|
|
265
272
|
);
|
|
266
273
|
WriteTodosToolUI.displayName = "WriteTodosToolUI";
|
|
267
274
|
exports.HiddenWriteTodosToolUI = HiddenWriteTodosToolUI;
|
|
275
|
+
exports.PLAN_STEP_STATUS_LABEL = PLAN_STEP_STATUS_LABEL;
|
|
268
276
|
exports.PlanStepMarker = PlanStepMarker;
|
|
269
277
|
exports.WriteTodosToolUI = WriteTodosToolUI;
|
|
270
278
|
exports.parseTodoPlan = parseTodoPlan;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"write-todos-tool-ui.cjs","sources":["../../src/tools/write-todos-tool-ui.tsx"],"sourcesContent":["\"use client\";\n\nimport { memo, useEffect, useMemo, useState } from \"react\";\nimport type { ToolCallMessagePartComponent } from \"@assistant-ui/react\";\nimport {\n CheckIcon,\n ChevronDownIcon,\n ListChecksIcon,\n LoaderIcon,\n XIcon,\n} from \"lucide-react\";\nimport { cn } from \"../lib/utils\";\nimport {\n allocatePlanSeq,\n registerPlanCard,\n unregisterPlanCard,\n updatePlanCard,\n useIsLatestPlanCard,\n usePlanScope,\n} from \"./todo-plan-store\";\n\n// ─── Types ───────────────────────────────────────────────────────────\n\nexport type PlanStepStatus =\n | \"pending\"\n | \"in_progress\"\n | \"completed\"\n | \"cancelled\"\n | \"failed\";\n\nexport type PlanStep = {\n id?: string;\n content: string;\n status: PlanStepStatus;\n};\n\nexport type TodoPlan = {\n steps: PlanStep[];\n changeDescription?: string;\n};\n\nconst STEP_STATUSES: readonly PlanStepStatus[] = [\n \"pending\",\n \"in_progress\",\n \"completed\",\n \"cancelled\",\n \"failed\",\n];\n\n// ─── Parsing ─────────────────────────────────────────────────────────\n\nfunction asRecord(value: unknown): Record<string, unknown> | null {\n return typeof value === \"object\" && value !== null\n ? (value as Record<string, unknown>)\n : null;\n}\n\nfunction asStatus(value: unknown): PlanStepStatus {\n return typeof value === \"string\" &&\n (STEP_STATUSES as readonly string[]).includes(value)\n ? (value as PlanStepStatus)\n : \"pending\";\n}\n\nfunction parseSteps(value: unknown): PlanStep[] {\n if (!Array.isArray(value)) return [];\n const steps: PlanStep[] = [];\n for (const raw of value) {\n const record = asRecord(raw);\n if (!record) continue;\n const content = record.content ?? record.task ?? record.title;\n if (typeof content !== \"string\" || content.trim() === \"\") continue;\n steps.push({\n id: typeof record.id === \"string\" ? record.id : undefined,\n content: content.trim(),\n status: asStatus(record.status),\n });\n }\n return steps;\n}\n\nfunction asDescription(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() !== \"\"\n ? value.trim()\n : undefined;\n}\n\nfunction parsePartialSteps(argsText: string | undefined): PlanStep[] {\n if (!argsText) return [];\n const steps: PlanStep[] = [];\n const objectPattern = /\\{(?:[^{}\\\\\"]|\"(?:[^\"\\\\]|\\\\.)*\")*\\}?/g;\n for (const [chunk] of argsText.matchAll(objectPattern)) {\n const contentMatch = /\"(?:content|task|title)\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"/.exec(\n chunk,\n );\n if (!contentMatch) continue;\n let content: string;\n try {\n content = JSON.parse(`\"${contentMatch[1]}\"`) as string;\n } catch {\n content = contentMatch[1];\n }\n if (content.trim() === \"\") continue;\n const statusMatch = /\"status\"\\s*:\\s*\"([a-z_]+)\"/.exec(chunk);\n steps.push({\n content: content.trim(),\n status: asStatus(statusMatch?.[1]),\n });\n }\n return steps;\n}\n\nexport function parseTodoPlan({\n args,\n argsText,\n result,\n}: {\n args?: unknown;\n argsText?: string;\n result?: unknown;\n}): TodoPlan {\n const argsRecord = asRecord(args);\n const argSteps = parseSteps(argsRecord?.todos);\n\n let resultRecord = asRecord(result);\n if (!resultRecord && typeof result === \"string\") {\n try {\n resultRecord = asRecord(JSON.parse(result));\n } catch {\n resultRecord = null;\n }\n }\n\n const hasResultTodos = Array.isArray(resultRecord?.todos);\n const steps = hasResultTodos\n ? parseSteps(resultRecord?.todos)\n : argSteps.length > 0\n ? argSteps\n : parsePartialSteps(argsText);\n\n return {\n steps,\n changeDescription:\n asDescription(resultRecord?.description) ??\n asDescription(resultRecord?.todoChangeDescription),\n };\n}\n\n// ─── Presentation ────────────────────────────────────────────────────\n\n/** A plan step's status glyph (its number while pending) — shared with the workbench's Tasks pane. */\nexport function PlanStepMarker({\n status,\n index,\n}: {\n status: PlanStepStatus;\n index: number;\n}) {\n if (status === \"completed\") {\n return (\n <span className=\"flex size-[18px] shrink-0 items-center justify-center rounded-full bg-emerald-500 text-white\">\n <CheckIcon className=\"size-3\" strokeWidth={3} />\n </span>\n );\n }\n if (status === \"in_progress\") {\n return (\n <span className=\"flex size-[18px] shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary\">\n <LoaderIcon className=\"size-3 animate-spin\" strokeWidth={3} />\n </span>\n );\n }\n if (status === \"failed\") {\n return (\n <span className=\"flex size-[18px] shrink-0 items-center justify-center rounded-full bg-destructive/10 text-destructive\">\n <XIcon className=\"size-3\" strokeWidth={3} />\n </span>\n );\n }\n if (status === \"cancelled\") {\n return (\n <span className=\"flex size-[18px] shrink-0 items-center justify-center rounded-full border border-border text-muted-foreground\">\n <XIcon className=\"size-3\" strokeWidth={3} />\n </span>\n );\n }\n return (\n <span className=\"flex size-[18px] shrink-0 items-center justify-center rounded-full border border-dashed border-border text-[9px] font-medium text-muted-foreground\">\n {index + 1}\n </span>\n );\n}\n\nfunction PlanBadge({\n resolved,\n total,\n}: {\n resolved: number;\n total: number;\n}) {\n const isDone = total > 0 && resolved === total;\n\n return (\n <span\n className={cn(\n \"flex size-8 shrink-0 items-center justify-center rounded-lg\",\n isDone && \"bg-emerald-50 text-emerald-600\",\n !isDone && \"bg-primary/10 text-primary\",\n )}\n >\n {isDone ? (\n <CheckIcon className=\"size-4\" strokeWidth={3} />\n ) : (\n <ListChecksIcon className=\"size-4\" />\n )}\n </span>\n );\n}\n\n/**\n * Register this card in its chat's plan registry and report whether it is the\n * newest mounted one. Shared by the visible card (which hides superseded\n * instances) and {@link HiddenWriteTodosToolUI} (which renders nothing but\n * keeps the registry — and so `useLatestTodoPlan` — fed).\n */\nfunction usePlanCardRegistration(plan: TodoPlan, isWriting: boolean): boolean {\n const scope = usePlanScope();\n const [seq] = useState(allocatePlanSeq);\n useEffect(() => {\n registerPlanCard(scope, seq, plan, isWriting);\n return () => unregisterPlanCard(scope, seq);\n // Registration is keyed to this instance's lifetime (and its chat's\n // scope); content updates ride the effect below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [scope, seq]);\n useEffect(() => {\n updatePlanCard(scope, seq, plan, isWriting);\n }, [scope, seq, plan, isWriting]);\n return useIsLatestPlanCard(scope, seq);\n}\n\n/**\n * Inline-invisible `write_todos` for hosts that pin the plan elsewhere:\n * feeds the plan registry exactly like the visible card, renders nothing.\n * Register it via `toolUIs={{ write_todos: HiddenWriteTodosToolUI }}` and\n * read the plan with `useLatestTodoPlan()` — a bare `() => null` override\n * would leave the registry (and the hook) permanently empty.\n */\nconst HiddenWriteTodosToolUIImpl: ToolCallMessagePartComponent = ({\n args,\n argsText,\n result,\n status,\n}) => {\n const plan = useMemo(\n () => parseTodoPlan({ args, argsText, result }),\n [args, argsText, result],\n );\n usePlanCardRegistration(plan, status?.type === \"running\");\n return null;\n};\n\nexport const HiddenWriteTodosToolUI = memo(\n HiddenWriteTodosToolUIImpl,\n) as unknown as ToolCallMessagePartComponent;\nHiddenWriteTodosToolUI.displayName = \"HiddenWriteTodosToolUI\";\n\nconst WriteTodosToolUIImpl: ToolCallMessagePartComponent = ({\n args,\n argsText,\n result,\n status,\n}) => {\n const plan = useMemo(\n () => parseTodoPlan({ args, argsText, result }),\n [args, argsText, result],\n );\n const { steps, changeDescription } = plan;\n const [collapsed, setCollapsed] = useState(false);\n\n // Newest-plan dedupe: a run updates its plan by calling write_todos again,\n // so a conversation stacks cards that all describe the same plan. Only the\n // newest mounted card in this chat's scope renders (see todo-plan-store.ts).\n const isWriting = status?.type === \"running\";\n const isLatest = usePlanCardRegistration(plan, isWriting);\n if (!isLatest) return null;\n\n const completed = steps.filter((s) => s.status === \"completed\").length;\n const stopped = steps.filter(\n (s) => s.status === \"cancelled\" || s.status === \"failed\",\n ).length;\n const current = steps.find((s) => s.status === \"in_progress\");\n const total = steps.length;\n const resolved = completed + stopped;\n const isDone = total > 0 && resolved === total;\n\n if (total === 0) {\n return isWriting ? (\n <div className=\"my-3 flex w-full items-center gap-2 rounded-xl border border-border/60 bg-background px-4 py-3 shadow-sm\">\n <LoaderIcon className=\"size-3.5 animate-spin text-primary\" />\n <span className=\"text-[13px] font-medium text-foreground\">\n Planning...\n </span>\n </div>\n ) : null;\n }\n\n const heading = isWriting\n ? \"Updating plan\"\n : isDone\n ? \"Plan complete\"\n : completed === 0 && !current\n ? \"Plan\"\n : \"Plan in progress\";\n\n const subtitle =\n changeDescription ?? (current ? current.content : undefined);\n\n return (\n <div className=\"my-3 w-full rounded-xl border border-border/60 bg-background shadow-sm\">\n <div className=\"sticky top-0 z-20 overflow-hidden rounded-t-xl bg-background\">\n <button\n type=\"button\"\n onClick={() => setCollapsed((prev) => !prev)}\n className=\"flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-muted/30\"\n aria-expanded={!collapsed}\n >\n <PlanBadge resolved={resolved} total={total} />\n <span className=\"min-w-0 flex-1\">\n <span className=\"flex items-baseline gap-2\">\n <span className=\"text-[13px] font-medium text-foreground\">\n {heading}\n </span>\n <span className=\"text-[11px] tabular-nums text-muted-foreground\">\n {resolved}/{total} steps\n </span>\n </span>\n {subtitle && (\n <span className=\"block truncate text-[12px] text-muted-foreground\">\n {subtitle}\n </span>\n )}\n </span>\n <ChevronDownIcon\n className={cn(\n \"size-4 shrink-0 text-muted-foreground transition-transform duration-200\",\n collapsed && \"-rotate-90\",\n )}\n />\n </button>\n\n <div className=\"h-0.5 w-full bg-muted\">\n <div\n className={cn(\n \"h-full transition-all duration-500\",\n isDone ? \"bg-emerald-500\" : \"bg-primary\",\n )}\n style={{ width: `${total > 0 ? (resolved / total) * 100 : 0}%` }}\n />\n </div>\n </div>\n\n {!collapsed && (\n <ol className=\"flex max-h-72 flex-col overflow-y-auto rounded-b-xl border-t border-border/40 py-1\">\n {steps.map((step, index) => (\n <li\n key={step.id ?? `${index}-${step.content}`}\n className={cn(\n \"relative flex items-start gap-2.5 px-4 py-1.5\",\n step.status === \"in_progress\" && \"bg-primary/5\",\n )}\n >\n {index < steps.length - 1 && (\n <span\n aria-hidden\n className=\"absolute left-[calc(1rem+8px)] top-[26px] h-[calc(100%-18px)] w-px bg-border/60\"\n />\n )}\n <span className=\"relative z-10 mt-[3px]\">\n <PlanStepMarker status={step.status} index={index} />\n </span>\n <span\n className={cn(\n \"text-[12.5px] leading-relaxed text-foreground\",\n step.status === \"completed\" &&\n \"text-muted-foreground line-through decoration-muted-foreground/40\",\n step.status === \"cancelled\" &&\n \"text-muted-foreground/70 line-through decoration-muted-foreground/40\",\n step.status === \"failed\" && \"text-destructive\",\n step.status === \"in_progress\" && \"font-medium text-primary\",\n step.status === \"pending\" && \"text-muted-foreground\",\n )}\n >\n {step.content}\n </span>\n </li>\n ))}\n </ol>\n )}\n </div>\n );\n};\n\nexport const WriteTodosToolUI = memo(\n WriteTodosToolUIImpl,\n) as unknown as ToolCallMessagePartComponent;\nWriteTodosToolUI.displayName = \"WriteTodosToolUI\";\n"],"names":["jsx","CheckIcon","LoaderIcon","XIcon","cn","ListChecksIcon","usePlanScope","useState","allocatePlanSeq","useEffect","registerPlanCard","unregisterPlanCard","updatePlanCard","useIsLatestPlanCard","useMemo","memo","jsxs","ChevronDownIcon"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"write-todos-tool-ui.cjs","sources":["../../src/tools/write-todos-tool-ui.tsx"],"sourcesContent":["\"use client\";\n\nimport { memo, useEffect, useMemo, useState } from \"react\";\nimport type { ToolCallMessagePartComponent } from \"@assistant-ui/react\";\nimport {\n CheckIcon,\n ChevronDownIcon,\n ListChecksIcon,\n LoaderIcon,\n XIcon,\n} from \"lucide-react\";\nimport { cn } from \"../lib/utils\";\nimport {\n allocatePlanSeq,\n registerPlanCard,\n unregisterPlanCard,\n updatePlanCard,\n useIsLatestPlanCard,\n usePlanScope,\n} from \"./todo-plan-store\";\n\n// ─── Types ───────────────────────────────────────────────────────────\n\nexport type PlanStepStatus =\n | \"pending\"\n | \"in_progress\"\n | \"completed\"\n | \"cancelled\"\n | \"failed\";\n\nexport type PlanStep = {\n id?: string;\n content: string;\n status: PlanStepStatus;\n};\n\nexport type TodoPlan = {\n steps: PlanStep[];\n changeDescription?: string;\n};\n\nconst STEP_STATUSES: readonly PlanStepStatus[] = [\n \"pending\",\n \"in_progress\",\n \"completed\",\n \"cancelled\",\n \"failed\",\n];\n\n// ─── Parsing ─────────────────────────────────────────────────────────\n\nfunction asRecord(value: unknown): Record<string, unknown> | null {\n return typeof value === \"object\" && value !== null\n ? (value as Record<string, unknown>)\n : null;\n}\n\nfunction asStatus(value: unknown): PlanStepStatus {\n return typeof value === \"string\" &&\n (STEP_STATUSES as readonly string[]).includes(value)\n ? (value as PlanStepStatus)\n : \"pending\";\n}\n\nfunction parseSteps(value: unknown): PlanStep[] {\n if (!Array.isArray(value)) return [];\n const steps: PlanStep[] = [];\n for (const raw of value) {\n const record = asRecord(raw);\n if (!record) continue;\n const content = record.content ?? record.task ?? record.title;\n if (typeof content !== \"string\" || content.trim() === \"\") continue;\n steps.push({\n id: typeof record.id === \"string\" ? record.id : undefined,\n content: content.trim(),\n status: asStatus(record.status),\n });\n }\n return steps;\n}\n\nfunction asDescription(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() !== \"\"\n ? value.trim()\n : undefined;\n}\n\nfunction parsePartialSteps(argsText: string | undefined): PlanStep[] {\n if (!argsText) return [];\n const steps: PlanStep[] = [];\n const objectPattern = /\\{(?:[^{}\\\\\"]|\"(?:[^\"\\\\]|\\\\.)*\")*\\}?/g;\n for (const [chunk] of argsText.matchAll(objectPattern)) {\n const contentMatch = /\"(?:content|task|title)\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"/.exec(\n chunk,\n );\n if (!contentMatch) continue;\n let content: string;\n try {\n content = JSON.parse(`\"${contentMatch[1]}\"`) as string;\n } catch {\n content = contentMatch[1];\n }\n if (content.trim() === \"\") continue;\n const statusMatch = /\"status\"\\s*:\\s*\"([a-z_]+)\"/.exec(chunk);\n steps.push({\n content: content.trim(),\n status: asStatus(statusMatch?.[1]),\n });\n }\n return steps;\n}\n\nexport function parseTodoPlan({\n args,\n argsText,\n result,\n}: {\n args?: unknown;\n argsText?: string;\n result?: unknown;\n}): TodoPlan {\n const argsRecord = asRecord(args);\n const argSteps = parseSteps(argsRecord?.todos);\n\n let resultRecord = asRecord(result);\n if (!resultRecord && typeof result === \"string\") {\n try {\n resultRecord = asRecord(JSON.parse(result));\n } catch {\n resultRecord = null;\n }\n }\n\n const hasResultTodos = Array.isArray(resultRecord?.todos);\n const steps = hasResultTodos\n ? parseSteps(resultRecord?.todos)\n : argSteps.length > 0\n ? argSteps\n : parsePartialSteps(argsText);\n\n return {\n steps,\n changeDescription:\n asDescription(resultRecord?.description) ??\n asDescription(resultRecord?.todoChangeDescription),\n };\n}\n\n// ─── Presentation ────────────────────────────────────────────────────\n\n/** A plan step's status in words, for screen readers beside its glyph (the Tasks pane, the plan strip). */\nexport const PLAN_STEP_STATUS_LABEL: Record<PlanStepStatus, string> = {\n pending: \"To do\",\n in_progress: \"In progress\",\n completed: \"Done\",\n failed: \"Failed\",\n cancelled: \"Cancelled\",\n};\n\n/** A plan step's status glyph (its number while pending) — shared with the workbench's Tasks pane. */\nexport function PlanStepMarker({\n status,\n index,\n}: {\n status: PlanStepStatus;\n index: number;\n}) {\n if (status === \"completed\") {\n return (\n <span className=\"flex size-[18px] shrink-0 items-center justify-center rounded-full bg-emerald-500 text-white\">\n <CheckIcon className=\"size-3\" strokeWidth={3} />\n </span>\n );\n }\n if (status === \"in_progress\") {\n return (\n <span className=\"flex size-[18px] shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary\">\n <LoaderIcon className=\"size-3 animate-spin\" strokeWidth={3} />\n </span>\n );\n }\n if (status === \"failed\") {\n return (\n <span className=\"flex size-[18px] shrink-0 items-center justify-center rounded-full bg-destructive/10 text-destructive\">\n <XIcon className=\"size-3\" strokeWidth={3} />\n </span>\n );\n }\n if (status === \"cancelled\") {\n return (\n <span className=\"flex size-[18px] shrink-0 items-center justify-center rounded-full border border-border text-muted-foreground\">\n <XIcon className=\"size-3\" strokeWidth={3} />\n </span>\n );\n }\n return (\n <span className=\"flex size-[18px] shrink-0 items-center justify-center rounded-full border border-dashed border-border text-[9px] font-medium text-muted-foreground\">\n {index + 1}\n </span>\n );\n}\n\nfunction PlanBadge({\n resolved,\n total,\n}: {\n resolved: number;\n total: number;\n}) {\n const isDone = total > 0 && resolved === total;\n\n return (\n <span\n className={cn(\n \"flex size-8 shrink-0 items-center justify-center rounded-lg\",\n isDone && \"bg-emerald-50 text-emerald-600\",\n !isDone && \"bg-primary/10 text-primary\",\n )}\n >\n {isDone ? (\n <CheckIcon className=\"size-4\" strokeWidth={3} />\n ) : (\n <ListChecksIcon className=\"size-4\" />\n )}\n </span>\n );\n}\n\n/**\n * Register this card in its chat's plan registry and report whether it is the\n * newest mounted one. Shared by the visible card (which hides superseded\n * instances) and {@link HiddenWriteTodosToolUI} (which renders nothing but\n * keeps the registry — and so `useLatestTodoPlan` — fed).\n */\nfunction usePlanCardRegistration(plan: TodoPlan, isWriting: boolean): boolean {\n const scope = usePlanScope();\n const [seq] = useState(allocatePlanSeq);\n useEffect(() => {\n registerPlanCard(scope, seq, plan, isWriting);\n return () => unregisterPlanCard(scope, seq);\n // Registration is keyed to this instance's lifetime (and its chat's\n // scope); content updates ride the effect below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [scope, seq]);\n useEffect(() => {\n updatePlanCard(scope, seq, plan, isWriting);\n }, [scope, seq, plan, isWriting]);\n return useIsLatestPlanCard(scope, seq);\n}\n\n/**\n * Inline-invisible `write_todos` for hosts that pin the plan elsewhere:\n * feeds the plan registry exactly like the visible card, renders nothing.\n * Register it via `toolUIs={{ write_todos: HiddenWriteTodosToolUI }}` and\n * read the plan with `useLatestTodoPlan()` — a bare `() => null` override\n * would leave the registry (and the hook) permanently empty.\n */\nconst HiddenWriteTodosToolUIImpl: ToolCallMessagePartComponent = ({\n args,\n argsText,\n result,\n status,\n}) => {\n const plan = useMemo(\n () => parseTodoPlan({ args, argsText, result }),\n [args, argsText, result],\n );\n usePlanCardRegistration(plan, status?.type === \"running\");\n return null;\n};\n\nexport const HiddenWriteTodosToolUI = memo(\n HiddenWriteTodosToolUIImpl,\n) as unknown as ToolCallMessagePartComponent;\nHiddenWriteTodosToolUI.displayName = \"HiddenWriteTodosToolUI\";\n\nconst WriteTodosToolUIImpl: ToolCallMessagePartComponent = ({\n args,\n argsText,\n result,\n status,\n}) => {\n const plan = useMemo(\n () => parseTodoPlan({ args, argsText, result }),\n [args, argsText, result],\n );\n const { steps, changeDescription } = plan;\n const [collapsed, setCollapsed] = useState(false);\n\n // Newest-plan dedupe: a run updates its plan by calling write_todos again,\n // so a conversation stacks cards that all describe the same plan. Only the\n // newest mounted card in this chat's scope renders (see todo-plan-store.ts).\n const isWriting = status?.type === \"running\";\n const isLatest = usePlanCardRegistration(plan, isWriting);\n if (!isLatest) return null;\n\n const completed = steps.filter((s) => s.status === \"completed\").length;\n const stopped = steps.filter(\n (s) => s.status === \"cancelled\" || s.status === \"failed\",\n ).length;\n const current = steps.find((s) => s.status === \"in_progress\");\n const total = steps.length;\n const resolved = completed + stopped;\n const isDone = total > 0 && resolved === total;\n\n if (total === 0) {\n return isWriting ? (\n <div className=\"my-3 flex w-full items-center gap-2 rounded-xl border border-border/60 bg-background px-4 py-3 shadow-sm\">\n <LoaderIcon className=\"size-3.5 animate-spin text-primary\" />\n <span className=\"text-[13px] font-medium text-foreground\">\n Planning...\n </span>\n </div>\n ) : null;\n }\n\n const heading = isWriting\n ? \"Updating plan\"\n : isDone\n ? \"Plan complete\"\n : completed === 0 && !current\n ? \"Plan\"\n : \"Plan in progress\";\n\n const subtitle =\n changeDescription ?? (current ? current.content : undefined);\n\n return (\n <div className=\"my-3 w-full rounded-xl border border-border/60 bg-background shadow-sm\">\n <div className=\"sticky top-0 z-20 overflow-hidden rounded-t-xl bg-background\">\n <button\n type=\"button\"\n onClick={() => setCollapsed((prev) => !prev)}\n className=\"flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-muted/30\"\n aria-expanded={!collapsed}\n >\n <PlanBadge resolved={resolved} total={total} />\n <span className=\"min-w-0 flex-1\">\n <span className=\"flex items-baseline gap-2\">\n <span className=\"text-[13px] font-medium text-foreground\">\n {heading}\n </span>\n <span className=\"text-[11px] tabular-nums text-muted-foreground\">\n {resolved}/{total} steps\n </span>\n </span>\n {subtitle && (\n <span className=\"block truncate text-[12px] text-muted-foreground\">\n {subtitle}\n </span>\n )}\n </span>\n <ChevronDownIcon\n className={cn(\n \"size-4 shrink-0 text-muted-foreground transition-transform duration-200\",\n collapsed && \"-rotate-90\",\n )}\n />\n </button>\n\n <div className=\"h-0.5 w-full bg-muted\">\n <div\n className={cn(\n \"h-full transition-all duration-500\",\n isDone ? \"bg-emerald-500\" : \"bg-primary\",\n )}\n style={{ width: `${total > 0 ? (resolved / total) * 100 : 0}%` }}\n />\n </div>\n </div>\n\n {!collapsed && (\n <ol className=\"flex max-h-72 flex-col overflow-y-auto rounded-b-xl border-t border-border/40 py-1\">\n {steps.map((step, index) => (\n <li\n key={step.id ?? `${index}-${step.content}`}\n className={cn(\n \"relative flex items-start gap-2.5 px-4 py-1.5\",\n step.status === \"in_progress\" && \"bg-primary/5\",\n )}\n >\n {index < steps.length - 1 && (\n <span\n aria-hidden\n className=\"absolute left-[calc(1rem+8px)] top-[26px] h-[calc(100%-18px)] w-px bg-border/60\"\n />\n )}\n <span className=\"relative z-10 mt-[3px]\">\n <PlanStepMarker status={step.status} index={index} />\n </span>\n <span\n className={cn(\n \"text-[12.5px] leading-relaxed text-foreground\",\n step.status === \"completed\" &&\n \"text-muted-foreground line-through decoration-muted-foreground/40\",\n step.status === \"cancelled\" &&\n \"text-muted-foreground/70 line-through decoration-muted-foreground/40\",\n step.status === \"failed\" && \"text-destructive\",\n step.status === \"in_progress\" && \"font-medium text-primary\",\n step.status === \"pending\" && \"text-muted-foreground\",\n )}\n >\n {step.content}\n </span>\n </li>\n ))}\n </ol>\n )}\n </div>\n );\n};\n\nexport const WriteTodosToolUI = memo(\n WriteTodosToolUIImpl,\n) as unknown as ToolCallMessagePartComponent;\nWriteTodosToolUI.displayName = \"WriteTodosToolUI\";\n"],"names":["jsx","CheckIcon","LoaderIcon","XIcon","cn","ListChecksIcon","usePlanScope","useState","allocatePlanSeq","useEffect","registerPlanCard","unregisterPlanCard","updatePlanCard","useIsLatestPlanCard","useMemo","memo","jsxs","ChevronDownIcon"],"mappings":"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"}
|
|
@@ -14,6 +14,8 @@ export declare function parseTodoPlan({ args, argsText, result, }: {
|
|
|
14
14
|
argsText?: string;
|
|
15
15
|
result?: unknown;
|
|
16
16
|
}): TodoPlan;
|
|
17
|
+
/** A plan step's status in words, for screen readers beside its glyph (the Tasks pane, the plan strip). */
|
|
18
|
+
export declare const PLAN_STEP_STATUS_LABEL: Record<PlanStepStatus, string>;
|
|
17
19
|
/** A plan step's status glyph (its number while pending) — shared with the workbench's Tasks pane. */
|
|
18
20
|
export declare function PlanStepMarker({ status, index, }: {
|
|
19
21
|
status: PlanStepStatus;
|
|
@@ -81,6 +81,13 @@ function parseTodoPlan({
|
|
|
81
81
|
changeDescription: asDescription(resultRecord == null ? void 0 : resultRecord.description) ?? asDescription(resultRecord == null ? void 0 : resultRecord.todoChangeDescription)
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
|
+
const PLAN_STEP_STATUS_LABEL = {
|
|
85
|
+
pending: "To do",
|
|
86
|
+
in_progress: "In progress",
|
|
87
|
+
completed: "Done",
|
|
88
|
+
failed: "Failed",
|
|
89
|
+
cancelled: "Cancelled"
|
|
90
|
+
};
|
|
84
91
|
function PlanStepMarker({
|
|
85
92
|
status,
|
|
86
93
|
index
|
|
@@ -264,6 +271,7 @@ const WriteTodosToolUI = memo(
|
|
|
264
271
|
WriteTodosToolUI.displayName = "WriteTodosToolUI";
|
|
265
272
|
export {
|
|
266
273
|
HiddenWriteTodosToolUI,
|
|
274
|
+
PLAN_STEP_STATUS_LABEL,
|
|
267
275
|
PlanStepMarker,
|
|
268
276
|
WriteTodosToolUI,
|
|
269
277
|
parseTodoPlan
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"write-todos-tool-ui.js","sources":["../../src/tools/write-todos-tool-ui.tsx"],"sourcesContent":["\"use client\";\n\nimport { memo, useEffect, useMemo, useState } from \"react\";\nimport type { ToolCallMessagePartComponent } from \"@assistant-ui/react\";\nimport {\n CheckIcon,\n ChevronDownIcon,\n ListChecksIcon,\n LoaderIcon,\n XIcon,\n} from \"lucide-react\";\nimport { cn } from \"../lib/utils\";\nimport {\n allocatePlanSeq,\n registerPlanCard,\n unregisterPlanCard,\n updatePlanCard,\n useIsLatestPlanCard,\n usePlanScope,\n} from \"./todo-plan-store\";\n\n// ─── Types ───────────────────────────────────────────────────────────\n\nexport type PlanStepStatus =\n | \"pending\"\n | \"in_progress\"\n | \"completed\"\n | \"cancelled\"\n | \"failed\";\n\nexport type PlanStep = {\n id?: string;\n content: string;\n status: PlanStepStatus;\n};\n\nexport type TodoPlan = {\n steps: PlanStep[];\n changeDescription?: string;\n};\n\nconst STEP_STATUSES: readonly PlanStepStatus[] = [\n \"pending\",\n \"in_progress\",\n \"completed\",\n \"cancelled\",\n \"failed\",\n];\n\n// ─── Parsing ─────────────────────────────────────────────────────────\n\nfunction asRecord(value: unknown): Record<string, unknown> | null {\n return typeof value === \"object\" && value !== null\n ? (value as Record<string, unknown>)\n : null;\n}\n\nfunction asStatus(value: unknown): PlanStepStatus {\n return typeof value === \"string\" &&\n (STEP_STATUSES as readonly string[]).includes(value)\n ? (value as PlanStepStatus)\n : \"pending\";\n}\n\nfunction parseSteps(value: unknown): PlanStep[] {\n if (!Array.isArray(value)) return [];\n const steps: PlanStep[] = [];\n for (const raw of value) {\n const record = asRecord(raw);\n if (!record) continue;\n const content = record.content ?? record.task ?? record.title;\n if (typeof content !== \"string\" || content.trim() === \"\") continue;\n steps.push({\n id: typeof record.id === \"string\" ? record.id : undefined,\n content: content.trim(),\n status: asStatus(record.status),\n });\n }\n return steps;\n}\n\nfunction asDescription(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() !== \"\"\n ? value.trim()\n : undefined;\n}\n\nfunction parsePartialSteps(argsText: string | undefined): PlanStep[] {\n if (!argsText) return [];\n const steps: PlanStep[] = [];\n const objectPattern = /\\{(?:[^{}\\\\\"]|\"(?:[^\"\\\\]|\\\\.)*\")*\\}?/g;\n for (const [chunk] of argsText.matchAll(objectPattern)) {\n const contentMatch = /\"(?:content|task|title)\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"/.exec(\n chunk,\n );\n if (!contentMatch) continue;\n let content: string;\n try {\n content = JSON.parse(`\"${contentMatch[1]}\"`) as string;\n } catch {\n content = contentMatch[1];\n }\n if (content.trim() === \"\") continue;\n const statusMatch = /\"status\"\\s*:\\s*\"([a-z_]+)\"/.exec(chunk);\n steps.push({\n content: content.trim(),\n status: asStatus(statusMatch?.[1]),\n });\n }\n return steps;\n}\n\nexport function parseTodoPlan({\n args,\n argsText,\n result,\n}: {\n args?: unknown;\n argsText?: string;\n result?: unknown;\n}): TodoPlan {\n const argsRecord = asRecord(args);\n const argSteps = parseSteps(argsRecord?.todos);\n\n let resultRecord = asRecord(result);\n if (!resultRecord && typeof result === \"string\") {\n try {\n resultRecord = asRecord(JSON.parse(result));\n } catch {\n resultRecord = null;\n }\n }\n\n const hasResultTodos = Array.isArray(resultRecord?.todos);\n const steps = hasResultTodos\n ? parseSteps(resultRecord?.todos)\n : argSteps.length > 0\n ? argSteps\n : parsePartialSteps(argsText);\n\n return {\n steps,\n changeDescription:\n asDescription(resultRecord?.description) ??\n asDescription(resultRecord?.todoChangeDescription),\n };\n}\n\n// ─── Presentation ────────────────────────────────────────────────────\n\n/** A plan step's status glyph (its number while pending) — shared with the workbench's Tasks pane. */\nexport function PlanStepMarker({\n status,\n index,\n}: {\n status: PlanStepStatus;\n index: number;\n}) {\n if (status === \"completed\") {\n return (\n <span className=\"flex size-[18px] shrink-0 items-center justify-center rounded-full bg-emerald-500 text-white\">\n <CheckIcon className=\"size-3\" strokeWidth={3} />\n </span>\n );\n }\n if (status === \"in_progress\") {\n return (\n <span className=\"flex size-[18px] shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary\">\n <LoaderIcon className=\"size-3 animate-spin\" strokeWidth={3} />\n </span>\n );\n }\n if (status === \"failed\") {\n return (\n <span className=\"flex size-[18px] shrink-0 items-center justify-center rounded-full bg-destructive/10 text-destructive\">\n <XIcon className=\"size-3\" strokeWidth={3} />\n </span>\n );\n }\n if (status === \"cancelled\") {\n return (\n <span className=\"flex size-[18px] shrink-0 items-center justify-center rounded-full border border-border text-muted-foreground\">\n <XIcon className=\"size-3\" strokeWidth={3} />\n </span>\n );\n }\n return (\n <span className=\"flex size-[18px] shrink-0 items-center justify-center rounded-full border border-dashed border-border text-[9px] font-medium text-muted-foreground\">\n {index + 1}\n </span>\n );\n}\n\nfunction PlanBadge({\n resolved,\n total,\n}: {\n resolved: number;\n total: number;\n}) {\n const isDone = total > 0 && resolved === total;\n\n return (\n <span\n className={cn(\n \"flex size-8 shrink-0 items-center justify-center rounded-lg\",\n isDone && \"bg-emerald-50 text-emerald-600\",\n !isDone && \"bg-primary/10 text-primary\",\n )}\n >\n {isDone ? (\n <CheckIcon className=\"size-4\" strokeWidth={3} />\n ) : (\n <ListChecksIcon className=\"size-4\" />\n )}\n </span>\n );\n}\n\n/**\n * Register this card in its chat's plan registry and report whether it is the\n * newest mounted one. Shared by the visible card (which hides superseded\n * instances) and {@link HiddenWriteTodosToolUI} (which renders nothing but\n * keeps the registry — and so `useLatestTodoPlan` — fed).\n */\nfunction usePlanCardRegistration(plan: TodoPlan, isWriting: boolean): boolean {\n const scope = usePlanScope();\n const [seq] = useState(allocatePlanSeq);\n useEffect(() => {\n registerPlanCard(scope, seq, plan, isWriting);\n return () => unregisterPlanCard(scope, seq);\n // Registration is keyed to this instance's lifetime (and its chat's\n // scope); content updates ride the effect below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [scope, seq]);\n useEffect(() => {\n updatePlanCard(scope, seq, plan, isWriting);\n }, [scope, seq, plan, isWriting]);\n return useIsLatestPlanCard(scope, seq);\n}\n\n/**\n * Inline-invisible `write_todos` for hosts that pin the plan elsewhere:\n * feeds the plan registry exactly like the visible card, renders nothing.\n * Register it via `toolUIs={{ write_todos: HiddenWriteTodosToolUI }}` and\n * read the plan with `useLatestTodoPlan()` — a bare `() => null` override\n * would leave the registry (and the hook) permanently empty.\n */\nconst HiddenWriteTodosToolUIImpl: ToolCallMessagePartComponent = ({\n args,\n argsText,\n result,\n status,\n}) => {\n const plan = useMemo(\n () => parseTodoPlan({ args, argsText, result }),\n [args, argsText, result],\n );\n usePlanCardRegistration(plan, status?.type === \"running\");\n return null;\n};\n\nexport const HiddenWriteTodosToolUI = memo(\n HiddenWriteTodosToolUIImpl,\n) as unknown as ToolCallMessagePartComponent;\nHiddenWriteTodosToolUI.displayName = \"HiddenWriteTodosToolUI\";\n\nconst WriteTodosToolUIImpl: ToolCallMessagePartComponent = ({\n args,\n argsText,\n result,\n status,\n}) => {\n const plan = useMemo(\n () => parseTodoPlan({ args, argsText, result }),\n [args, argsText, result],\n );\n const { steps, changeDescription } = plan;\n const [collapsed, setCollapsed] = useState(false);\n\n // Newest-plan dedupe: a run updates its plan by calling write_todos again,\n // so a conversation stacks cards that all describe the same plan. Only the\n // newest mounted card in this chat's scope renders (see todo-plan-store.ts).\n const isWriting = status?.type === \"running\";\n const isLatest = usePlanCardRegistration(plan, isWriting);\n if (!isLatest) return null;\n\n const completed = steps.filter((s) => s.status === \"completed\").length;\n const stopped = steps.filter(\n (s) => s.status === \"cancelled\" || s.status === \"failed\",\n ).length;\n const current = steps.find((s) => s.status === \"in_progress\");\n const total = steps.length;\n const resolved = completed + stopped;\n const isDone = total > 0 && resolved === total;\n\n if (total === 0) {\n return isWriting ? (\n <div className=\"my-3 flex w-full items-center gap-2 rounded-xl border border-border/60 bg-background px-4 py-3 shadow-sm\">\n <LoaderIcon className=\"size-3.5 animate-spin text-primary\" />\n <span className=\"text-[13px] font-medium text-foreground\">\n Planning...\n </span>\n </div>\n ) : null;\n }\n\n const heading = isWriting\n ? \"Updating plan\"\n : isDone\n ? \"Plan complete\"\n : completed === 0 && !current\n ? \"Plan\"\n : \"Plan in progress\";\n\n const subtitle =\n changeDescription ?? (current ? current.content : undefined);\n\n return (\n <div className=\"my-3 w-full rounded-xl border border-border/60 bg-background shadow-sm\">\n <div className=\"sticky top-0 z-20 overflow-hidden rounded-t-xl bg-background\">\n <button\n type=\"button\"\n onClick={() => setCollapsed((prev) => !prev)}\n className=\"flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-muted/30\"\n aria-expanded={!collapsed}\n >\n <PlanBadge resolved={resolved} total={total} />\n <span className=\"min-w-0 flex-1\">\n <span className=\"flex items-baseline gap-2\">\n <span className=\"text-[13px] font-medium text-foreground\">\n {heading}\n </span>\n <span className=\"text-[11px] tabular-nums text-muted-foreground\">\n {resolved}/{total} steps\n </span>\n </span>\n {subtitle && (\n <span className=\"block truncate text-[12px] text-muted-foreground\">\n {subtitle}\n </span>\n )}\n </span>\n <ChevronDownIcon\n className={cn(\n \"size-4 shrink-0 text-muted-foreground transition-transform duration-200\",\n collapsed && \"-rotate-90\",\n )}\n />\n </button>\n\n <div className=\"h-0.5 w-full bg-muted\">\n <div\n className={cn(\n \"h-full transition-all duration-500\",\n isDone ? \"bg-emerald-500\" : \"bg-primary\",\n )}\n style={{ width: `${total > 0 ? (resolved / total) * 100 : 0}%` }}\n />\n </div>\n </div>\n\n {!collapsed && (\n <ol className=\"flex max-h-72 flex-col overflow-y-auto rounded-b-xl border-t border-border/40 py-1\">\n {steps.map((step, index) => (\n <li\n key={step.id ?? `${index}-${step.content}`}\n className={cn(\n \"relative flex items-start gap-2.5 px-4 py-1.5\",\n step.status === \"in_progress\" && \"bg-primary/5\",\n )}\n >\n {index < steps.length - 1 && (\n <span\n aria-hidden\n className=\"absolute left-[calc(1rem+8px)] top-[26px] h-[calc(100%-18px)] w-px bg-border/60\"\n />\n )}\n <span className=\"relative z-10 mt-[3px]\">\n <PlanStepMarker status={step.status} index={index} />\n </span>\n <span\n className={cn(\n \"text-[12.5px] leading-relaxed text-foreground\",\n step.status === \"completed\" &&\n \"text-muted-foreground line-through decoration-muted-foreground/40\",\n step.status === \"cancelled\" &&\n \"text-muted-foreground/70 line-through decoration-muted-foreground/40\",\n step.status === \"failed\" && \"text-destructive\",\n step.status === \"in_progress\" && \"font-medium text-primary\",\n step.status === \"pending\" && \"text-muted-foreground\",\n )}\n >\n {step.content}\n </span>\n </li>\n ))}\n </ol>\n )}\n </div>\n );\n};\n\nexport const WriteTodosToolUI = memo(\n WriteTodosToolUIImpl,\n) as unknown as ToolCallMessagePartComponent;\nWriteTodosToolUI.displayName = \"WriteTodosToolUI\";\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"write-todos-tool-ui.js","sources":["../../src/tools/write-todos-tool-ui.tsx"],"sourcesContent":["\"use client\";\n\nimport { memo, useEffect, useMemo, useState } from \"react\";\nimport type { ToolCallMessagePartComponent } from \"@assistant-ui/react\";\nimport {\n CheckIcon,\n ChevronDownIcon,\n ListChecksIcon,\n LoaderIcon,\n XIcon,\n} from \"lucide-react\";\nimport { cn } from \"../lib/utils\";\nimport {\n allocatePlanSeq,\n registerPlanCard,\n unregisterPlanCard,\n updatePlanCard,\n useIsLatestPlanCard,\n usePlanScope,\n} from \"./todo-plan-store\";\n\n// ─── Types ───────────────────────────────────────────────────────────\n\nexport type PlanStepStatus =\n | \"pending\"\n | \"in_progress\"\n | \"completed\"\n | \"cancelled\"\n | \"failed\";\n\nexport type PlanStep = {\n id?: string;\n content: string;\n status: PlanStepStatus;\n};\n\nexport type TodoPlan = {\n steps: PlanStep[];\n changeDescription?: string;\n};\n\nconst STEP_STATUSES: readonly PlanStepStatus[] = [\n \"pending\",\n \"in_progress\",\n \"completed\",\n \"cancelled\",\n \"failed\",\n];\n\n// ─── Parsing ─────────────────────────────────────────────────────────\n\nfunction asRecord(value: unknown): Record<string, unknown> | null {\n return typeof value === \"object\" && value !== null\n ? (value as Record<string, unknown>)\n : null;\n}\n\nfunction asStatus(value: unknown): PlanStepStatus {\n return typeof value === \"string\" &&\n (STEP_STATUSES as readonly string[]).includes(value)\n ? (value as PlanStepStatus)\n : \"pending\";\n}\n\nfunction parseSteps(value: unknown): PlanStep[] {\n if (!Array.isArray(value)) return [];\n const steps: PlanStep[] = [];\n for (const raw of value) {\n const record = asRecord(raw);\n if (!record) continue;\n const content = record.content ?? record.task ?? record.title;\n if (typeof content !== \"string\" || content.trim() === \"\") continue;\n steps.push({\n id: typeof record.id === \"string\" ? record.id : undefined,\n content: content.trim(),\n status: asStatus(record.status),\n });\n }\n return steps;\n}\n\nfunction asDescription(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() !== \"\"\n ? value.trim()\n : undefined;\n}\n\nfunction parsePartialSteps(argsText: string | undefined): PlanStep[] {\n if (!argsText) return [];\n const steps: PlanStep[] = [];\n const objectPattern = /\\{(?:[^{}\\\\\"]|\"(?:[^\"\\\\]|\\\\.)*\")*\\}?/g;\n for (const [chunk] of argsText.matchAll(objectPattern)) {\n const contentMatch = /\"(?:content|task|title)\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"/.exec(\n chunk,\n );\n if (!contentMatch) continue;\n let content: string;\n try {\n content = JSON.parse(`\"${contentMatch[1]}\"`) as string;\n } catch {\n content = contentMatch[1];\n }\n if (content.trim() === \"\") continue;\n const statusMatch = /\"status\"\\s*:\\s*\"([a-z_]+)\"/.exec(chunk);\n steps.push({\n content: content.trim(),\n status: asStatus(statusMatch?.[1]),\n });\n }\n return steps;\n}\n\nexport function parseTodoPlan({\n args,\n argsText,\n result,\n}: {\n args?: unknown;\n argsText?: string;\n result?: unknown;\n}): TodoPlan {\n const argsRecord = asRecord(args);\n const argSteps = parseSteps(argsRecord?.todos);\n\n let resultRecord = asRecord(result);\n if (!resultRecord && typeof result === \"string\") {\n try {\n resultRecord = asRecord(JSON.parse(result));\n } catch {\n resultRecord = null;\n }\n }\n\n const hasResultTodos = Array.isArray(resultRecord?.todos);\n const steps = hasResultTodos\n ? parseSteps(resultRecord?.todos)\n : argSteps.length > 0\n ? argSteps\n : parsePartialSteps(argsText);\n\n return {\n steps,\n changeDescription:\n asDescription(resultRecord?.description) ??\n asDescription(resultRecord?.todoChangeDescription),\n };\n}\n\n// ─── Presentation ────────────────────────────────────────────────────\n\n/** A plan step's status in words, for screen readers beside its glyph (the Tasks pane, the plan strip). */\nexport const PLAN_STEP_STATUS_LABEL: Record<PlanStepStatus, string> = {\n pending: \"To do\",\n in_progress: \"In progress\",\n completed: \"Done\",\n failed: \"Failed\",\n cancelled: \"Cancelled\",\n};\n\n/** A plan step's status glyph (its number while pending) — shared with the workbench's Tasks pane. */\nexport function PlanStepMarker({\n status,\n index,\n}: {\n status: PlanStepStatus;\n index: number;\n}) {\n if (status === \"completed\") {\n return (\n <span className=\"flex size-[18px] shrink-0 items-center justify-center rounded-full bg-emerald-500 text-white\">\n <CheckIcon className=\"size-3\" strokeWidth={3} />\n </span>\n );\n }\n if (status === \"in_progress\") {\n return (\n <span className=\"flex size-[18px] shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary\">\n <LoaderIcon className=\"size-3 animate-spin\" strokeWidth={3} />\n </span>\n );\n }\n if (status === \"failed\") {\n return (\n <span className=\"flex size-[18px] shrink-0 items-center justify-center rounded-full bg-destructive/10 text-destructive\">\n <XIcon className=\"size-3\" strokeWidth={3} />\n </span>\n );\n }\n if (status === \"cancelled\") {\n return (\n <span className=\"flex size-[18px] shrink-0 items-center justify-center rounded-full border border-border text-muted-foreground\">\n <XIcon className=\"size-3\" strokeWidth={3} />\n </span>\n );\n }\n return (\n <span className=\"flex size-[18px] shrink-0 items-center justify-center rounded-full border border-dashed border-border text-[9px] font-medium text-muted-foreground\">\n {index + 1}\n </span>\n );\n}\n\nfunction PlanBadge({\n resolved,\n total,\n}: {\n resolved: number;\n total: number;\n}) {\n const isDone = total > 0 && resolved === total;\n\n return (\n <span\n className={cn(\n \"flex size-8 shrink-0 items-center justify-center rounded-lg\",\n isDone && \"bg-emerald-50 text-emerald-600\",\n !isDone && \"bg-primary/10 text-primary\",\n )}\n >\n {isDone ? (\n <CheckIcon className=\"size-4\" strokeWidth={3} />\n ) : (\n <ListChecksIcon className=\"size-4\" />\n )}\n </span>\n );\n}\n\n/**\n * Register this card in its chat's plan registry and report whether it is the\n * newest mounted one. Shared by the visible card (which hides superseded\n * instances) and {@link HiddenWriteTodosToolUI} (which renders nothing but\n * keeps the registry — and so `useLatestTodoPlan` — fed).\n */\nfunction usePlanCardRegistration(plan: TodoPlan, isWriting: boolean): boolean {\n const scope = usePlanScope();\n const [seq] = useState(allocatePlanSeq);\n useEffect(() => {\n registerPlanCard(scope, seq, plan, isWriting);\n return () => unregisterPlanCard(scope, seq);\n // Registration is keyed to this instance's lifetime (and its chat's\n // scope); content updates ride the effect below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [scope, seq]);\n useEffect(() => {\n updatePlanCard(scope, seq, plan, isWriting);\n }, [scope, seq, plan, isWriting]);\n return useIsLatestPlanCard(scope, seq);\n}\n\n/**\n * Inline-invisible `write_todos` for hosts that pin the plan elsewhere:\n * feeds the plan registry exactly like the visible card, renders nothing.\n * Register it via `toolUIs={{ write_todos: HiddenWriteTodosToolUI }}` and\n * read the plan with `useLatestTodoPlan()` — a bare `() => null` override\n * would leave the registry (and the hook) permanently empty.\n */\nconst HiddenWriteTodosToolUIImpl: ToolCallMessagePartComponent = ({\n args,\n argsText,\n result,\n status,\n}) => {\n const plan = useMemo(\n () => parseTodoPlan({ args, argsText, result }),\n [args, argsText, result],\n );\n usePlanCardRegistration(plan, status?.type === \"running\");\n return null;\n};\n\nexport const HiddenWriteTodosToolUI = memo(\n HiddenWriteTodosToolUIImpl,\n) as unknown as ToolCallMessagePartComponent;\nHiddenWriteTodosToolUI.displayName = \"HiddenWriteTodosToolUI\";\n\nconst WriteTodosToolUIImpl: ToolCallMessagePartComponent = ({\n args,\n argsText,\n result,\n status,\n}) => {\n const plan = useMemo(\n () => parseTodoPlan({ args, argsText, result }),\n [args, argsText, result],\n );\n const { steps, changeDescription } = plan;\n const [collapsed, setCollapsed] = useState(false);\n\n // Newest-plan dedupe: a run updates its plan by calling write_todos again,\n // so a conversation stacks cards that all describe the same plan. Only the\n // newest mounted card in this chat's scope renders (see todo-plan-store.ts).\n const isWriting = status?.type === \"running\";\n const isLatest = usePlanCardRegistration(plan, isWriting);\n if (!isLatest) return null;\n\n const completed = steps.filter((s) => s.status === \"completed\").length;\n const stopped = steps.filter(\n (s) => s.status === \"cancelled\" || s.status === \"failed\",\n ).length;\n const current = steps.find((s) => s.status === \"in_progress\");\n const total = steps.length;\n const resolved = completed + stopped;\n const isDone = total > 0 && resolved === total;\n\n if (total === 0) {\n return isWriting ? (\n <div className=\"my-3 flex w-full items-center gap-2 rounded-xl border border-border/60 bg-background px-4 py-3 shadow-sm\">\n <LoaderIcon className=\"size-3.5 animate-spin text-primary\" />\n <span className=\"text-[13px] font-medium text-foreground\">\n Planning...\n </span>\n </div>\n ) : null;\n }\n\n const heading = isWriting\n ? \"Updating plan\"\n : isDone\n ? \"Plan complete\"\n : completed === 0 && !current\n ? \"Plan\"\n : \"Plan in progress\";\n\n const subtitle =\n changeDescription ?? (current ? current.content : undefined);\n\n return (\n <div className=\"my-3 w-full rounded-xl border border-border/60 bg-background shadow-sm\">\n <div className=\"sticky top-0 z-20 overflow-hidden rounded-t-xl bg-background\">\n <button\n type=\"button\"\n onClick={() => setCollapsed((prev) => !prev)}\n className=\"flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-muted/30\"\n aria-expanded={!collapsed}\n >\n <PlanBadge resolved={resolved} total={total} />\n <span className=\"min-w-0 flex-1\">\n <span className=\"flex items-baseline gap-2\">\n <span className=\"text-[13px] font-medium text-foreground\">\n {heading}\n </span>\n <span className=\"text-[11px] tabular-nums text-muted-foreground\">\n {resolved}/{total} steps\n </span>\n </span>\n {subtitle && (\n <span className=\"block truncate text-[12px] text-muted-foreground\">\n {subtitle}\n </span>\n )}\n </span>\n <ChevronDownIcon\n className={cn(\n \"size-4 shrink-0 text-muted-foreground transition-transform duration-200\",\n collapsed && \"-rotate-90\",\n )}\n />\n </button>\n\n <div className=\"h-0.5 w-full bg-muted\">\n <div\n className={cn(\n \"h-full transition-all duration-500\",\n isDone ? \"bg-emerald-500\" : \"bg-primary\",\n )}\n style={{ width: `${total > 0 ? (resolved / total) * 100 : 0}%` }}\n />\n </div>\n </div>\n\n {!collapsed && (\n <ol className=\"flex max-h-72 flex-col overflow-y-auto rounded-b-xl border-t border-border/40 py-1\">\n {steps.map((step, index) => (\n <li\n key={step.id ?? `${index}-${step.content}`}\n className={cn(\n \"relative flex items-start gap-2.5 px-4 py-1.5\",\n step.status === \"in_progress\" && \"bg-primary/5\",\n )}\n >\n {index < steps.length - 1 && (\n <span\n aria-hidden\n className=\"absolute left-[calc(1rem+8px)] top-[26px] h-[calc(100%-18px)] w-px bg-border/60\"\n />\n )}\n <span className=\"relative z-10 mt-[3px]\">\n <PlanStepMarker status={step.status} index={index} />\n </span>\n <span\n className={cn(\n \"text-[12.5px] leading-relaxed text-foreground\",\n step.status === \"completed\" &&\n \"text-muted-foreground line-through decoration-muted-foreground/40\",\n step.status === \"cancelled\" &&\n \"text-muted-foreground/70 line-through decoration-muted-foreground/40\",\n step.status === \"failed\" && \"text-destructive\",\n step.status === \"in_progress\" && \"font-medium text-primary\",\n step.status === \"pending\" && \"text-muted-foreground\",\n )}\n >\n {step.content}\n </span>\n </li>\n ))}\n </ol>\n )}\n </div>\n );\n};\n\nexport const WriteTodosToolUI = memo(\n WriteTodosToolUIImpl,\n) as unknown as ToolCallMessagePartComponent;\nWriteTodosToolUI.displayName = \"WriteTodosToolUI\";\n"],"names":[],"mappings":"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"}
|
|
@@ -51,13 +51,6 @@ const RequiresStatewireThread = ({
|
|
|
51
51
|
}
|
|
52
52
|
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
|
|
53
53
|
};
|
|
54
|
-
const TASK_STATUS_LABEL = {
|
|
55
|
-
pending: "To do",
|
|
56
|
-
in_progress: "In progress",
|
|
57
|
-
completed: "Done",
|
|
58
|
-
failed: "Failed",
|
|
59
|
-
cancelled: "Cancelled"
|
|
60
|
-
};
|
|
61
54
|
const TasksPaneContent = () => {
|
|
62
55
|
const stateTodos = reactStatewire.useStatewireState((state) => statewireState.readDeepAgentStreamValues(state).todos);
|
|
63
56
|
const messages = react.useAuiState((state) => state.thread.messages);
|
|
@@ -94,7 +87,7 @@ const TasksPaneContent = () => {
|
|
|
94
87
|
/* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: "size-[18px] shrink-0 rounded-full border border-border border-dashed" })
|
|
95
88
|
) : /* @__PURE__ */ jsxRuntime.jsx(writeTodosToolUi.PlanStepMarker, { status: step.status, index }),
|
|
96
89
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sr-only", children: [
|
|
97
|
-
|
|
90
|
+
writeTodosToolUi.PLAN_STEP_STATUS_LABEL[step.status],
|
|
98
91
|
": "
|
|
99
92
|
] }),
|
|
100
93
|
/* @__PURE__ */ jsxRuntime.jsxs(
|