@agent-native/dispatch 0.40.36 → 0.40.38
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/CHANGELOG.md +25 -14
- package/dist/actions/provider-api-register.d.ts +11 -11
- package/dist/components/layout/Layout.d.ts +0 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +57 -101
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/routes/pages/overview.d.ts +2 -2
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +9 -5
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/community-apps.js +1 -1
- package/dist/server/lib/community-apps.js.map +1 -1
- package/dist/server/lib/usage-metrics-store.js +2 -2
- package/dist/server/lib/usage-metrics-store.js.map +1 -1
- package/package.json +3 -3
- package/src/components/layout/Layout.spec.tsx +348 -26
- package/src/components/layout/Layout.tsx +119 -193
- package/src/server/lib/app-creation-store.spec.ts +26 -5
- package/src/server/lib/app-creation-store.ts +10 -6
- package/src/server/lib/community-apps.ts +1 -1
- package/src/server/lib/usage-metrics-store.spec.ts +6 -2
- package/src/server/lib/usage-metrics-store.ts +2 -2
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
useAgentChatHomeHandoff,
|
|
28
28
|
useAgentChatHomeHandoffLinks,
|
|
29
29
|
useChatThreads,
|
|
30
|
-
type ChatThreadSummary,
|
|
31
30
|
} from "@agent-native/core/client/agent-chat";
|
|
32
31
|
import { appBasePath, appPath } from "@agent-native/core/client/api-path";
|
|
33
32
|
import {
|
|
@@ -55,6 +54,11 @@ import type {
|
|
|
55
54
|
ChatFirstCopy,
|
|
56
55
|
ChatFirstEmbedTarget,
|
|
57
56
|
} from "@agent-native/toolkit/app/chat/chat-first/types";
|
|
57
|
+
import {
|
|
58
|
+
OwnershipChatHistoryRail,
|
|
59
|
+
ownDraftRows,
|
|
60
|
+
useOwnershipChatHistory,
|
|
61
|
+
} from "@agent-native/toolkit/app/chat/ownership-history";
|
|
58
62
|
import { FeedbackButton } from "@agent-native/toolkit/app/feedback";
|
|
59
63
|
import { InvitationBanner, OrgSwitcher } from "@agent-native/toolkit/app/org";
|
|
60
64
|
import { RunsTray } from "@agent-native/toolkit/app/progress";
|
|
@@ -64,10 +68,6 @@ import {
|
|
|
64
68
|
AppSidebarHeader,
|
|
65
69
|
openCommandMenu,
|
|
66
70
|
} from "@agent-native/toolkit/app/shared";
|
|
67
|
-
import {
|
|
68
|
-
ChatHistoryRail,
|
|
69
|
-
type ChatHistoryItem,
|
|
70
|
-
} from "@agent-native/toolkit/chat-history";
|
|
71
71
|
import { writeClipboardText } from "@agent-native/toolkit/clipboard";
|
|
72
72
|
import {
|
|
73
73
|
IconApps,
|
|
@@ -143,7 +143,6 @@ import {
|
|
|
143
143
|
} from "../deferred-chat-components.js";
|
|
144
144
|
import { Button } from "../ui/button";
|
|
145
145
|
import { Sheet, SheetContent, SheetDescription, SheetTitle } from "../ui/sheet";
|
|
146
|
-
import { Skeleton } from "../ui/skeleton";
|
|
147
146
|
import {
|
|
148
147
|
Tooltip,
|
|
149
148
|
TooltipContent,
|
|
@@ -588,23 +587,6 @@ function threadIdFromPath(pathname: string): string | null {
|
|
|
588
587
|
}
|
|
589
588
|
}
|
|
590
589
|
|
|
591
|
-
export function formatThreadAge(updatedAt: number, now = Date.now()) {
|
|
592
|
-
const diffMs = Math.max(0, now - updatedAt);
|
|
593
|
-
const minutes = Math.floor(diffMs / 60_000);
|
|
594
|
-
if (minutes < 1) return "now";
|
|
595
|
-
if (minutes < 60) return `${minutes}m`;
|
|
596
|
-
const hours = Math.floor(minutes / 60);
|
|
597
|
-
if (hours < 24) return `${hours}h`;
|
|
598
|
-
const days = Math.floor(hours / 24);
|
|
599
|
-
if (days < 14) return `${days}d`;
|
|
600
|
-
if (days < 365) return `${Math.floor(days / 7)}w`;
|
|
601
|
-
return `${Math.floor(days / 365)}y`;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function threadTitle(thread: ChatThreadSummary, fallback: string) {
|
|
605
|
-
return thread.title || thread.preview || fallback;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
590
|
function threadSourceIcon(platform: string | undefined): ReactNode {
|
|
609
591
|
const normalized = platform?.trim().toLowerCase();
|
|
610
592
|
if (!normalized) return null;
|
|
@@ -627,14 +609,6 @@ function readChatHistoryIncludesExternal(): boolean {
|
|
|
627
609
|
}
|
|
628
610
|
}
|
|
629
611
|
|
|
630
|
-
function threadUpdatedAt(thread: ChatThreadSummary) {
|
|
631
|
-
return Number.isFinite(thread.updatedAt)
|
|
632
|
-
? thread.updatedAt
|
|
633
|
-
: Number.isFinite(thread.createdAt)
|
|
634
|
-
? thread.createdAt
|
|
635
|
-
: 0;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
612
|
function DispatchChatsSection({
|
|
639
613
|
onNavigate,
|
|
640
614
|
showNewChat = false,
|
|
@@ -663,78 +637,74 @@ function DispatchChatsSection({
|
|
|
663
637
|
const location = useLocation();
|
|
664
638
|
const [includeExternal, setIncludeExternal] = useState(false);
|
|
665
639
|
const [historyPreferenceReady, setHistoryPreferenceReady] = useState(false);
|
|
666
|
-
const historyModeRef = useRef(includeExternal);
|
|
667
640
|
const {
|
|
668
641
|
threads,
|
|
669
642
|
activeThreadId,
|
|
670
|
-
isLoading: chatsLoading,
|
|
671
|
-
threadsLoadError,
|
|
672
643
|
createThread,
|
|
673
644
|
switchThread,
|
|
674
645
|
renameThread,
|
|
675
|
-
|
|
646
|
+
pinThread,
|
|
647
|
+
archiveThread,
|
|
648
|
+
isNewThread,
|
|
676
649
|
} = useChatThreads(undefined, "dispatch", undefined, {
|
|
677
650
|
autoCreate: false,
|
|
678
651
|
includeExternal,
|
|
679
652
|
});
|
|
680
653
|
|
|
681
|
-
const visibleThreads = useMemo(
|
|
682
|
-
() =>
|
|
683
|
-
threads
|
|
684
|
-
.filter(
|
|
685
|
-
(thread) => thread.messageCount > 0 || thread.id === activeThreadId,
|
|
686
|
-
)
|
|
687
|
-
.sort((a, b) => threadUpdatedAt(b) - threadUpdatedAt(a))
|
|
688
|
-
.slice(0, 8),
|
|
689
|
-
[activeThreadId, threads],
|
|
690
|
-
);
|
|
691
654
|
const localPathname = localDispatchPath(location.pathname);
|
|
692
655
|
const displayedActiveThreadId =
|
|
693
656
|
threadIdFromPath(localPathname) ??
|
|
694
657
|
(localPathname === "/chat" ? null : activeThreadId);
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
658
|
+
const history = useOwnershipChatHistory({
|
|
659
|
+
getThreadUrl: (threadId) => appPath(chatThreadPath(threadId)),
|
|
660
|
+
includeExternal,
|
|
661
|
+
activeId: displayedActiveThreadId,
|
|
662
|
+
localMineRows: ownDraftRows({
|
|
663
|
+
threads,
|
|
664
|
+
isNewThread,
|
|
665
|
+
activeId: activeThreadId,
|
|
666
|
+
}),
|
|
667
|
+
mutations: { pin: pinThread, rename: renameThread, archive: archiveThread },
|
|
668
|
+
onMutationFailed: () =>
|
|
669
|
+
toast.error(t("dispatch.integrations.failedToSave")),
|
|
670
|
+
decorateItem: (item, row) => {
|
|
671
|
+
const source = "source" in row ? row.source : undefined;
|
|
672
|
+
const sourceLabel = source?.platform
|
|
673
|
+
? source.platform[0].toUpperCase() + source.platform.slice(1)
|
|
674
|
+
: undefined;
|
|
675
|
+
const sourceIcon = threadSourceIcon(source?.platform);
|
|
676
|
+
return {
|
|
677
|
+
...item,
|
|
678
|
+
title: (
|
|
679
|
+
<span
|
|
680
|
+
className="flex min-w-0 items-center gap-1"
|
|
681
|
+
title={
|
|
682
|
+
sourceLabel ? `${sourceLabel}: ${item.titleText}` : item.titleText
|
|
683
|
+
}
|
|
684
|
+
>
|
|
685
|
+
{sourceIcon ? (
|
|
686
|
+
<span
|
|
687
|
+
className="shrink-0 text-sidebar-foreground/55"
|
|
688
|
+
aria-label={sourceLabel}
|
|
689
|
+
>
|
|
690
|
+
{sourceIcon}
|
|
691
|
+
</span>
|
|
692
|
+
) : null}
|
|
693
|
+
<span className="truncate">{item.title}</span>
|
|
694
|
+
</span>
|
|
695
|
+
),
|
|
696
|
+
};
|
|
697
|
+
},
|
|
726
698
|
});
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
) : undefined;
|
|
737
|
-
|
|
699
|
+
const listProps = {
|
|
700
|
+
...history.listProps,
|
|
701
|
+
onDelete: async (threadId: string) => {
|
|
702
|
+
const archived = await history.listProps.onDelete?.(threadId);
|
|
703
|
+
if (archived && threadId === displayedActiveThreadId)
|
|
704
|
+
await handleNewChat();
|
|
705
|
+
return archived === true;
|
|
706
|
+
},
|
|
707
|
+
};
|
|
738
708
|
useEffect(() => {
|
|
739
709
|
setIncludeExternal(readChatHistoryIncludesExternal());
|
|
740
710
|
setHistoryPreferenceReady(true);
|
|
@@ -748,30 +718,7 @@ function DispatchChatsSection({
|
|
|
748
718
|
includeExternal ? "all" : "local",
|
|
749
719
|
);
|
|
750
720
|
} catch {} // coercion-ok: localStorage is optional browser persistence.
|
|
751
|
-
|
|
752
|
-
historyModeRef.current = includeExternal;
|
|
753
|
-
refreshThreads();
|
|
754
|
-
}
|
|
755
|
-
}, [historyPreferenceReady, includeExternal, refreshThreads]);
|
|
756
|
-
|
|
757
|
-
useEffect(() => {
|
|
758
|
-
const refresh = () => refreshThreads();
|
|
759
|
-
const handleRunning = (event: Event) => {
|
|
760
|
-
const detail = (event as CustomEvent).detail as
|
|
761
|
-
| { isRunning?: unknown }
|
|
762
|
-
| undefined;
|
|
763
|
-
if (detail?.isRunning === false) refreshThreads();
|
|
764
|
-
};
|
|
765
|
-
|
|
766
|
-
window.addEventListener("agent-chat:threads-updated", refresh);
|
|
767
|
-
window.addEventListener("agentNative.chatRunning", handleRunning);
|
|
768
|
-
window.addEventListener("focus", refresh);
|
|
769
|
-
return () => {
|
|
770
|
-
window.removeEventListener("agent-chat:threads-updated", refresh);
|
|
771
|
-
window.removeEventListener("agentNative.chatRunning", handleRunning);
|
|
772
|
-
window.removeEventListener("focus", refresh);
|
|
773
|
-
};
|
|
774
|
-
}, [refreshThreads]);
|
|
721
|
+
}, [historyPreferenceReady, includeExternal]);
|
|
775
722
|
|
|
776
723
|
function openThread(threadId: string, options?: { isNew?: boolean }) {
|
|
777
724
|
switchThread(threadId);
|
|
@@ -796,6 +743,36 @@ function DispatchChatsSection({
|
|
|
796
743
|
if (threadId) openThread(threadId, { isNew: true });
|
|
797
744
|
}
|
|
798
745
|
|
|
746
|
+
const sourceToggle = (
|
|
747
|
+
<Tooltip>
|
|
748
|
+
<TooltipTrigger asChild>
|
|
749
|
+
<button
|
|
750
|
+
type="button"
|
|
751
|
+
data-dispatch-chat-source-toggle
|
|
752
|
+
aria-pressed={includeExternal}
|
|
753
|
+
aria-label={
|
|
754
|
+
includeExternal
|
|
755
|
+
? t("dispatch.sidebar.showLocalChats")
|
|
756
|
+
: t("dispatch.sidebar.showAllChats")
|
|
757
|
+
}
|
|
758
|
+
onClick={() => setIncludeExternal((current) => !current)}
|
|
759
|
+
className="flex size-6 items-center justify-center rounded-md text-sidebar-foreground/55 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
|
760
|
+
>
|
|
761
|
+
{includeExternal ? (
|
|
762
|
+
<IconWorld size={14} aria-hidden="true" />
|
|
763
|
+
) : (
|
|
764
|
+
<IconDeviceDesktop size={14} aria-hidden="true" />
|
|
765
|
+
)}
|
|
766
|
+
</button>
|
|
767
|
+
</TooltipTrigger>
|
|
768
|
+
<TooltipContent side="right">
|
|
769
|
+
{includeExternal
|
|
770
|
+
? t("dispatch.sidebar.showLocalChats")
|
|
771
|
+
: t("dispatch.sidebar.showAllChats")}
|
|
772
|
+
</TooltipContent>
|
|
773
|
+
</Tooltip>
|
|
774
|
+
);
|
|
775
|
+
|
|
799
776
|
const collapsedChatFirst = collapsed && chatFirstMode;
|
|
800
777
|
|
|
801
778
|
return (
|
|
@@ -822,80 +799,26 @@ function DispatchChatsSection({
|
|
|
822
799
|
</nav>
|
|
823
800
|
) : null}
|
|
824
801
|
{prelude}
|
|
825
|
-
{!collapsedChatFirst
|
|
826
|
-
<div className="flex justify-end px-2 pt-0.5">
|
|
827
|
-
<Tooltip>
|
|
828
|
-
<TooltipTrigger asChild>
|
|
829
|
-
<button
|
|
830
|
-
type="button"
|
|
831
|
-
data-dispatch-chat-source-toggle
|
|
832
|
-
aria-pressed={includeExternal}
|
|
833
|
-
aria-label={
|
|
834
|
-
includeExternal
|
|
835
|
-
? t("dispatch.sidebar.showLocalChats", {
|
|
836
|
-
defaultValue: "Show local chats",
|
|
837
|
-
})
|
|
838
|
-
: t("dispatch.sidebar.showAllChats", {
|
|
839
|
-
defaultValue: "Show all chats",
|
|
840
|
-
})
|
|
841
|
-
}
|
|
842
|
-
onClick={() => setIncludeExternal((current) => !current)}
|
|
843
|
-
className="flex size-6 items-center justify-center rounded-md text-sidebar-foreground/55 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
|
844
|
-
>
|
|
845
|
-
{includeExternal ? (
|
|
846
|
-
<IconWorld size={14} aria-hidden="true" />
|
|
847
|
-
) : (
|
|
848
|
-
<IconDeviceDesktop size={14} aria-hidden="true" />
|
|
849
|
-
)}
|
|
850
|
-
</button>
|
|
851
|
-
</TooltipTrigger>
|
|
852
|
-
<TooltipContent side="right">
|
|
853
|
-
{includeExternal
|
|
854
|
-
? t("dispatch.sidebar.showLocalChats", {
|
|
855
|
-
defaultValue: "Show local chats",
|
|
856
|
-
})
|
|
857
|
-
: t("dispatch.sidebar.showAllChats", {
|
|
858
|
-
defaultValue: "Show all chats",
|
|
859
|
-
})}
|
|
860
|
-
</TooltipContent>
|
|
861
|
-
</Tooltip>
|
|
862
|
-
</div>
|
|
802
|
+
{!collapsedChatFirst ? (
|
|
803
|
+
<div className="flex justify-end px-2 pt-0.5">{sourceToggle}</div>
|
|
863
804
|
) : null}
|
|
864
|
-
{!collapsedChatFirst &&
|
|
865
|
-
!chatFirstMode &&
|
|
866
|
-
chatsLoading &&
|
|
867
|
-
visibleThreads.length === 0 &&
|
|
868
|
-
Array.from({ length: 3 }).map((_, index) => (
|
|
869
|
-
<div
|
|
870
|
-
key={`chat-skeleton-${index}`}
|
|
871
|
-
className="flex items-center gap-2 px-3 py-1"
|
|
872
|
-
>
|
|
873
|
-
<Skeleton className="size-3.5 shrink-0 rounded-sm" />
|
|
874
|
-
<Skeleton className="h-3 w-3/4 rounded" />
|
|
875
|
-
</div>
|
|
876
|
-
))}
|
|
877
805
|
{!collapsedChatFirst &&
|
|
878
806
|
chatFirstMode &&
|
|
879
|
-
(
|
|
807
|
+
(history.loading ||
|
|
808
|
+
history.sections.some(
|
|
809
|
+
(section) => section.items.length > 0 || section.error,
|
|
810
|
+
)) ? (
|
|
880
811
|
<ChatFirstChatHistory
|
|
881
|
-
|
|
812
|
+
sections={history.sections}
|
|
813
|
+
{...history.listProps}
|
|
814
|
+
onDelete={listProps.onDelete}
|
|
815
|
+
onNewChat={() => void handleNewChat()}
|
|
816
|
+
showNewChat={false}
|
|
817
|
+
railLabels={history.railLabels}
|
|
882
818
|
activeId={displayedActiveThreadId}
|
|
883
|
-
|
|
884
|
-
loadingLabel={
|
|
885
|
-
<div className="space-y-1 px-2 py-1">
|
|
886
|
-
{[0, 1, 2].map((index) => (
|
|
887
|
-
<div key={index} className="flex items-center gap-2 px-1 py-1">
|
|
888
|
-
<Skeleton className="size-3.5 shrink-0 rounded-sm" />
|
|
889
|
-
<Skeleton className="h-3 w-3/4 rounded" />
|
|
890
|
-
</div>
|
|
891
|
-
))}
|
|
892
|
-
</div>
|
|
893
|
-
}
|
|
894
|
-
label={t("dispatch.sidebar.chats", { defaultValue: "Chats" })}
|
|
895
|
-
error={chatHistoryError}
|
|
819
|
+
label={t("dispatch.sidebar.chats")}
|
|
896
820
|
onSelect={(threadId) => openThread(threadId)}
|
|
897
821
|
renameMaxLength={160}
|
|
898
|
-
onRename={(threadId, title) => void renameThread(threadId, title)}
|
|
899
822
|
labels={{
|
|
900
823
|
options: (item) =>
|
|
901
824
|
t("dispatch.sidebar.chatOptions", {
|
|
@@ -906,6 +829,9 @@ function DispatchChatsSection({
|
|
|
906
829
|
title: item.titleText ?? "",
|
|
907
830
|
}),
|
|
908
831
|
rename: t("dispatch.sidebar.renameChat"),
|
|
832
|
+
pin: t("agentChat.contextXray.pin"),
|
|
833
|
+
unpin: t("agentChat.contextXray.unpin"),
|
|
834
|
+
delete: t("extensions.delete"),
|
|
909
835
|
}}
|
|
910
836
|
renderAdditionalRowActions={(item, closeMenu) => (
|
|
911
837
|
<>
|
|
@@ -918,14 +844,14 @@ function DispatchChatsSection({
|
|
|
918
844
|
void writeClipboardText(item.id).then((copied) => {
|
|
919
845
|
toast[copied ? "success" : "error"](
|
|
920
846
|
copied
|
|
921
|
-
? "
|
|
922
|
-
: "
|
|
847
|
+
? chatFirstCopy("sessionIdCopied")
|
|
848
|
+
: t("agentChat.recovery.copyFailed"),
|
|
923
849
|
);
|
|
924
850
|
});
|
|
925
851
|
}}
|
|
926
852
|
>
|
|
927
853
|
<IconCopy size={13} strokeWidth={1.8} />
|
|
928
|
-
<span>
|
|
854
|
+
<span>{chatFirstCopy("copySessionId")}</span>
|
|
929
855
|
</button>
|
|
930
856
|
{!chatFirstEmbedded ? (
|
|
931
857
|
<button
|
|
@@ -943,27 +869,24 @@ function DispatchChatsSection({
|
|
|
943
869
|
}}
|
|
944
870
|
>
|
|
945
871
|
<IconEye size={13} strokeWidth={1.8} />
|
|
946
|
-
<span>
|
|
872
|
+
<span>{chatFirstCopy("watchSession")}</span>
|
|
947
873
|
</button>
|
|
948
874
|
) : null}
|
|
949
875
|
</>
|
|
950
876
|
)}
|
|
951
877
|
className="min-w-0 px-2"
|
|
952
878
|
/>
|
|
953
|
-
) : !collapsedChatFirst ? (
|
|
954
|
-
<
|
|
955
|
-
|
|
879
|
+
) : !collapsedChatFirst && !chatFirstMode ? (
|
|
880
|
+
<OwnershipChatHistoryRail
|
|
881
|
+
sections={history.sections}
|
|
882
|
+
listProps={listProps}
|
|
956
883
|
activeId={displayedActiveThreadId}
|
|
957
884
|
onSelect={(threadId) => openThread(threadId)}
|
|
958
885
|
onNewChat={() => void handleNewChat()}
|
|
959
886
|
railLabels={{
|
|
960
887
|
newChat: t("dispatch.sidebar.newChat"),
|
|
961
|
-
showMore: t("dispatch.sidebar.chats"),
|
|
962
|
-
showLess: t("dispatch.sidebar.chats"),
|
|
963
888
|
}}
|
|
964
|
-
error={chatHistoryError}
|
|
965
889
|
renameMaxLength={160}
|
|
966
|
-
onRename={(threadId, title) => void renameThread(threadId, title)}
|
|
967
890
|
labels={{
|
|
968
891
|
options: (item) =>
|
|
969
892
|
t("dispatch.sidebar.chatOptions", {
|
|
@@ -974,6 +897,9 @@ function DispatchChatsSection({
|
|
|
974
897
|
title: item.titleText ?? "",
|
|
975
898
|
}),
|
|
976
899
|
rename: t("dispatch.sidebar.renameChat"),
|
|
900
|
+
pin: t("agentChat.contextXray.pin"),
|
|
901
|
+
unpin: t("agentChat.contextXray.unpin"),
|
|
902
|
+
delete: t("extensions.delete"),
|
|
977
903
|
}}
|
|
978
904
|
renderAdditionalRowActions={(item, closeMenu) => (
|
|
979
905
|
<>
|
|
@@ -986,14 +912,14 @@ function DispatchChatsSection({
|
|
|
986
912
|
void writeClipboardText(item.id).then((copied) => {
|
|
987
913
|
toast[copied ? "success" : "error"](
|
|
988
914
|
copied
|
|
989
|
-
? "
|
|
990
|
-
: "
|
|
915
|
+
? chatFirstCopy("sessionIdCopied")
|
|
916
|
+
: t("agentChat.recovery.copyFailed"),
|
|
991
917
|
);
|
|
992
918
|
});
|
|
993
919
|
}}
|
|
994
920
|
>
|
|
995
921
|
<IconCopy size={13} strokeWidth={1.8} />
|
|
996
|
-
<span>
|
|
922
|
+
<span>{chatFirstCopy("copySessionId")}</span>
|
|
997
923
|
</button>
|
|
998
924
|
{chatFirstMode && !chatFirstEmbedded ? (
|
|
999
925
|
<button
|
|
@@ -1011,7 +937,7 @@ function DispatchChatsSection({
|
|
|
1011
937
|
}}
|
|
1012
938
|
>
|
|
1013
939
|
<IconEye size={13} strokeWidth={1.8} />
|
|
1014
|
-
<span>
|
|
940
|
+
<span>{chatFirstCopy("watchSession")}</span>
|
|
1015
941
|
</button>
|
|
1016
942
|
) : null}
|
|
1017
943
|
</>
|
|
@@ -636,7 +636,7 @@ describe("listWorkspaceApps", () => {
|
|
|
636
636
|
expect(apps[0]?.url).toBe("https://agent-workspace.builder.io/atlas");
|
|
637
637
|
});
|
|
638
638
|
|
|
639
|
-
it("signs the hosted registry request with the
|
|
639
|
+
it("signs the hosted registry request as the member with the deployment secret", async () => {
|
|
640
640
|
const fetchMock = vi.fn(
|
|
641
641
|
async () =>
|
|
642
642
|
new Response(JSON.stringify([]), {
|
|
@@ -656,15 +656,36 @@ describe("listWorkspaceApps", () => {
|
|
|
656
656
|
const authorization = fetchMock.mock.calls[0]?.[1]?.headers
|
|
657
657
|
?.Authorization as string;
|
|
658
658
|
const token = authorization.slice("Bearer ".length);
|
|
659
|
-
const payload = verifyJwtWithSecret(token, "
|
|
659
|
+
const payload = verifyJwtWithSecret(token, "dispatch-deployment-secret");
|
|
660
660
|
expect(payload).toMatchObject({
|
|
661
661
|
sub: "dev@example.test",
|
|
662
662
|
org_domain: "example.test",
|
|
663
663
|
aud: "https://agent-workspace.builder.io",
|
|
664
664
|
});
|
|
665
|
-
expect(() =>
|
|
666
|
-
|
|
667
|
-
|
|
665
|
+
expect(() => verifyJwtWithSecret(token, "workspace-org-secret")).toThrow();
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it("reports a missing deployment secret instead of signing a member's request with the organization secret", async () => {
|
|
669
|
+
const fetchMock = vi.fn();
|
|
670
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
671
|
+
vi.stubEnv("A2A_SECRET", "");
|
|
672
|
+
vi.stubEnv("WORKSPACE_GATEWAY_URL", "https://agent-workspace.builder.io");
|
|
673
|
+
mocks.getOrgA2ASecret.mockResolvedValueOnce("workspace-org-secret");
|
|
674
|
+
stubManifest([
|
|
675
|
+
{ id: "dispatch", name: "Dispatch", path: "/dispatch" },
|
|
676
|
+
{ id: "clips", name: "Clips", path: "/clips" },
|
|
677
|
+
]);
|
|
678
|
+
|
|
679
|
+
await expect(
|
|
680
|
+
runWithRequestContext(
|
|
681
|
+
{ userEmail: "dev@example.test", orgId: "org-1" },
|
|
682
|
+
() => listWorkspaceApps({ includeAgentCards: false }),
|
|
683
|
+
),
|
|
684
|
+
).rejects.toMatchObject({
|
|
685
|
+
statusCode: 503,
|
|
686
|
+
errorCode: "workspace_gateway_a2a_secret_missing",
|
|
687
|
+
});
|
|
688
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
668
689
|
});
|
|
669
690
|
|
|
670
691
|
it("reports missing hosted gateway signing credentials instead of sending anonymously", async () => {
|
|
@@ -1935,10 +1935,14 @@ async function readWorkspaceAppsFromGateway(): Promise<WorkspaceAppDiscovery | n
|
|
|
1935
1935
|
);
|
|
1936
1936
|
}
|
|
1937
1937
|
}
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1938
|
+
// The receiver drops the user from any user token that verifies only with an
|
|
1939
|
+
// organization secret, so a member's own request is signed with the
|
|
1940
|
+
// deployment secret to stay named.
|
|
1941
|
+
const delegatedMember =
|
|
1942
|
+
requestContext.a2aIdentityAssurance === "org-member";
|
|
1943
|
+
const signingSecret = delegatedMember ? orgSecret : getGlobalA2ASecret();
|
|
1944
|
+
const allowLocalUnauthenticatedRequest = isLocalGateway && !signingSecret;
|
|
1945
|
+
if (!signingSecret && !allowLocalUnauthenticatedRequest) {
|
|
1942
1946
|
throw new WorkspaceAppsGatewayAuthorizationError(
|
|
1943
1947
|
503,
|
|
1944
1948
|
"workspace_gateway_a2a_secret_missing",
|
|
@@ -1950,10 +1954,10 @@ async function readWorkspaceAppsFromGateway(): Promise<WorkspaceAppDiscovery | n
|
|
|
1950
1954
|
const token = await signA2AToken(
|
|
1951
1955
|
requestContext.userEmail,
|
|
1952
1956
|
orgDomain,
|
|
1953
|
-
orgSecret,
|
|
1957
|
+
delegatedMember ? orgSecret : undefined,
|
|
1954
1958
|
{
|
|
1955
1959
|
expiresIn: "1m",
|
|
1956
|
-
preferGlobalSecret:
|
|
1960
|
+
preferGlobalSecret: !delegatedMember,
|
|
1957
1961
|
audience: canonicalA2AAudience(
|
|
1958
1962
|
baseUrl.toString(),
|
|
1959
1963
|
getConfiguredAppBasePath() || undefined,
|
|
@@ -95,7 +95,7 @@ export const CURATED_COMMUNITY_APP_SOURCES: readonly CuratedCommunityAppSource[]
|
|
|
95
95
|
},
|
|
96
96
|
{
|
|
97
97
|
path: "package.json",
|
|
98
|
-
blobSha: "
|
|
98
|
+
blobSha: "b37b5659f115dd887f97611ff01e9861c004e0f8",
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
101
|
path: "server/db/index.ts",
|
|
@@ -173,7 +173,9 @@ describe("listDispatchUsageMetrics", () => {
|
|
|
173
173
|
errorCode: "usage_user_lookup_forbidden",
|
|
174
174
|
statusCode: 403,
|
|
175
175
|
});
|
|
176
|
-
expect((error as Error).message).toContain(
|
|
176
|
+
expect((error as Error).message).toContain(
|
|
177
|
+
"own customer or billing actions",
|
|
178
|
+
);
|
|
177
179
|
expect(mocks.isOrgMember).not.toHaveBeenCalled();
|
|
178
180
|
expect(mocks.getUsageSummary).not.toHaveBeenCalled();
|
|
179
181
|
},
|
|
@@ -530,7 +532,9 @@ describe("listDispatchUsageMetrics", () => {
|
|
|
530
532
|
expect((error as Error).message).toContain(
|
|
531
533
|
"customer@customer.example is not a member of this organization",
|
|
532
534
|
);
|
|
533
|
-
expect((error as Error).message).toContain(
|
|
535
|
+
expect((error as Error).message).toContain(
|
|
536
|
+
"own customer or billing actions",
|
|
537
|
+
);
|
|
534
538
|
expect((error as Error).message).toContain("describe-workspace-apps");
|
|
535
539
|
});
|
|
536
540
|
|
|
@@ -956,7 +956,7 @@ async function assertCanViewMetrics(viewScope: UsageMetricsScope): Promise<{
|
|
|
956
956
|
|
|
957
957
|
function failUserNotOrgMember(email: string): never {
|
|
958
958
|
fail(
|
|
959
|
-
`${email} is not a member of this organization
|
|
959
|
+
`${email} is not a member of this organization, so list-dispatch-usage-metrics has no data on them. For a customer's or account's usage or credits, use this app's own customer or billing actions if it has any, or find the app that owns that data with describe-workspace-apps query="<what you need>" and delegate with call-agent.`,
|
|
960
960
|
{ errorCode: "usage_user_not_org_member", statusCode: 404 },
|
|
961
961
|
);
|
|
962
962
|
}
|
|
@@ -988,7 +988,7 @@ export async function listDispatchUsageMetrics(input: {
|
|
|
988
988
|
callerRole !== "admin"
|
|
989
989
|
) {
|
|
990
990
|
fail(
|
|
991
|
-
|
|
991
|
+
`list-dispatch-usage-metrics cannot answer this. For a teammate in this organization, only owners and admins can look up their usage. For a customer or other external user, list-dispatch-usage-metrics has no data: use this app's own customer or billing actions if it has any, or find the app that owns that data with describe-workspace-apps query="<what you need>" and delegate with call-agent.`,
|
|
992
992
|
{ errorCode: "usage_user_lookup_forbidden", statusCode: 403 },
|
|
993
993
|
);
|
|
994
994
|
}
|