@agent-native/dispatch 0.22.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/components/app-icon.d.ts +2 -1
  2. package/dist/components/app-icon.d.ts.map +1 -1
  3. package/dist/components/app-icon.js +4 -2
  4. package/dist/components/app-icon.js.map +1 -1
  5. package/dist/components/app-list-row.d.ts +1 -1
  6. package/dist/components/app-list-row.d.ts.map +1 -1
  7. package/dist/components/app-list-row.js +1 -1
  8. package/dist/components/app-list-row.js.map +1 -1
  9. package/dist/components/app-open-actions.d.ts +2 -1
  10. package/dist/components/app-open-actions.d.ts.map +1 -1
  11. package/dist/components/app-open-actions.js +2 -2
  12. package/dist/components/app-open-actions.js.map +1 -1
  13. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  14. package/dist/components/dispatch-control-plane.js +16 -17
  15. package/dist/components/dispatch-control-plane.js.map +1 -1
  16. package/dist/components/layout/Layout.d.ts.map +1 -1
  17. package/dist/components/layout/Layout.js +84 -56
  18. package/dist/components/layout/Layout.js.map +1 -1
  19. package/dist/components/layout/workspace-apps-rail.d.ts.map +1 -1
  20. package/dist/components/layout/workspace-apps-rail.js +1 -1
  21. package/dist/components/layout/workspace-apps-rail.js.map +1 -1
  22. package/dist/components/other-apps-section.d.ts +3 -1
  23. package/dist/components/other-apps-section.d.ts.map +1 -1
  24. package/dist/components/other-apps-section.js +9 -6
  25. package/dist/components/other-apps-section.js.map +1 -1
  26. package/dist/components/workspace-app-card.d.ts.map +1 -1
  27. package/dist/components/workspace-app-card.js +7 -2
  28. package/dist/components/workspace-app-card.js.map +1 -1
  29. package/dist/components/workspace-template-card.d.ts.map +1 -1
  30. package/dist/components/workspace-template-card.js +2 -5
  31. package/dist/components/workspace-template-card.js.map +1 -1
  32. package/dist/lib/overview-chat.d.ts.map +1 -1
  33. package/dist/lib/overview-chat.js +0 -8
  34. package/dist/lib/overview-chat.js.map +1 -1
  35. package/package.json +3 -3
  36. package/src/components/app-icon.tsx +6 -2
  37. package/src/components/app-list-row.tsx +1 -1
  38. package/src/components/app-open-actions.tsx +4 -2
  39. package/src/components/dispatch-control-plane.spec.tsx +43 -7
  40. package/src/components/dispatch-control-plane.tsx +56 -75
  41. package/src/components/layout/Layout.spec.tsx +46 -0
  42. package/src/components/layout/Layout.tsx +149 -84
  43. package/src/components/layout/workspace-apps-rail.tsx +1 -0
  44. package/src/components/other-apps-section.tsx +61 -40
  45. package/src/components/workspace-app-card.spec.tsx +63 -66
  46. package/src/components/workspace-app-card.tsx +10 -3
  47. package/src/components/workspace-template-card.spec.tsx +7 -8
  48. package/src/components/workspace-template-card.tsx +21 -11
  49. package/src/lib/overview-chat.spec.ts +0 -19
  50. package/src/lib/overview-chat.ts +0 -9
  51. package/src/styles/dispatch.css +2 -2
@@ -214,6 +214,16 @@ interface ChatFirstEmbedSessionInput {
214
214
  chrome: "minimal";
215
215
  }
216
216
 
217
+ interface ChatFirstGrantedAppSummary {
218
+ id: string;
219
+ name: string;
220
+ url?: string | null;
221
+ }
222
+
223
+ interface ChatFirstGrantedAppsResult {
224
+ apps: ChatFirstGrantedAppSummary[];
225
+ }
226
+
217
227
  export function buildChatFirstEmbedSessionInput(
218
228
  appId: string,
219
229
  path: string,
@@ -663,7 +673,7 @@ function DispatchChatsSection({
663
673
  return (
664
674
  <div
665
675
  className={cn(
666
- "ms-4 min-w-0 space-y-0.5",
676
+ "min-w-0 space-y-0.5",
667
677
  showNewChat && "dispatch-chat-first-chats",
668
678
  )}
669
679
  >
@@ -730,7 +740,7 @@ function DispatchChatsSection({
730
740
  <Skeleton className="h-3 w-3/4 rounded" />
731
741
  </div>
732
742
  ))}
733
- {chatFirstMode ? (
743
+ {chatFirstMode && (chatsLoading || visibleThreads.length > 0) ? (
734
744
  <ChatFirstChatHistory
735
745
  items={chatItems}
736
746
  activeId={displayedActiveThreadId}
@@ -1015,6 +1025,7 @@ export function NavContent({
1015
1025
  id={app.id}
1016
1026
  name={app.name}
1017
1027
  size="sm"
1028
+ monochrome
1018
1029
  className="size-5 rounded-md"
1019
1030
  />
1020
1031
  )}
@@ -1278,7 +1289,11 @@ export function Layout({
1278
1289
  const isChatRoute =
1279
1290
  localPathname === "/chat" || localPathname.startsWith("/chat/");
1280
1291
  const chatFirstSurfaceScope = threadIdFromPath(localPathname) ?? "new";
1281
- const isWorkspaceAppHostRoute = localPathname.startsWith("/apps/");
1292
+ const isWorkspaceAppRoute = localPathname.startsWith("/apps/");
1293
+ const isWorkspaceAppHostRoute =
1294
+ isWorkspaceAppRoute &&
1295
+ typeof window !== "undefined" &&
1296
+ new URLSearchParams(window.location.search).get("embedded") === "1";
1282
1297
  const [chatFirstPreference, setChatFirstPreference] = useState(() =>
1283
1298
  readChatFirstMode(),
1284
1299
  );
@@ -1293,23 +1308,40 @@ export function Layout({
1293
1308
  const chatFirstAppsQuery = useActionQuery<WorkspaceAppSummary[]>(
1294
1309
  "list-workspace-apps",
1295
1310
  { includeAgentCards: false },
1296
- { enabled: chatFirstMode && isChatRoute },
1311
+ { enabled: chatFirstMode },
1312
+ );
1313
+ const chatFirstGrantedAppsQuery = useActionQuery<ChatFirstGrantedAppsResult>(
1314
+ "list_apps",
1315
+ {},
1316
+ { enabled: chatFirstMode },
1297
1317
  );
1298
1318
  const chatFirstWorkspaceApps = useMemo(
1299
1319
  () => mergeChatFirstWorkspaceApps(chatFirstAppsQuery.data),
1300
1320
  [chatFirstAppsQuery.data],
1301
1321
  );
1302
- const chatFirstAppRegistrations = useMemo<ChatFirstAppRegistration[]>(
1303
- () =>
1304
- chatFirstWorkspaceApps.map((app) => ({
1322
+ const chatFirstAppRegistrations = useMemo<ChatFirstAppRegistration[]>(() => {
1323
+ const registrations = new Map<string, ChatFirstAppRegistration>();
1324
+ for (const app of chatFirstWorkspaceApps) {
1325
+ registrations.set(app.id.toLowerCase(), {
1305
1326
  id: app.id,
1306
1327
  name: app.name,
1307
1328
  path: app.path,
1308
1329
  url: app.url,
1309
1330
  enabled: app.status !== "pending" && app.archived !== true,
1310
- })),
1311
- [chatFirstWorkspaceApps],
1312
- );
1331
+ });
1332
+ }
1333
+ for (const app of chatFirstGrantedAppsQuery.data?.apps ?? []) {
1334
+ const id = app.id.trim();
1335
+ if (!id || registrations.has(id.toLowerCase())) continue;
1336
+ registrations.set(id.toLowerCase(), {
1337
+ id,
1338
+ name: app.name,
1339
+ url: app.url,
1340
+ enabled: true,
1341
+ });
1342
+ }
1343
+ return [...registrations.values()];
1344
+ }, [chatFirstGrantedAppsQuery.data?.apps, chatFirstWorkspaceApps]);
1313
1345
  const chatFirstAppItems = useMemo<ChatFirstAppItem[]>(
1314
1346
  () =>
1315
1347
  chatFirstWorkspaceApps.map((app) => ({
@@ -1366,15 +1398,29 @@ export function Layout({
1366
1398
  ) ?? null,
1367
1399
  [chatFirstSurfaceTabs],
1368
1400
  );
1369
- const activeChatFirstApp = useMemo(
1401
+ const chatFirstAppTakesMain =
1402
+ chatFirstMode && isChatRoute && activeChatFirstSurfaceTab?.kind === "app";
1403
+ const activeChatFirstAppRegistration = useMemo(
1370
1404
  () =>
1371
1405
  activeChatFirstSurfaceTab?.kind === "app" &&
1372
1406
  activeChatFirstSurfaceTab.appId
1373
- ? (chatFirstWorkspaceApps.find(
1407
+ ? (chatFirstAppRegistrations.find(
1374
1408
  (app) => app.id === activeChatFirstSurfaceTab.appId,
1375
1409
  ) ?? null)
1376
1410
  : null,
1377
- [activeChatFirstSurfaceTab, chatFirstWorkspaceApps],
1411
+ [activeChatFirstSurfaceTab, chatFirstAppRegistrations],
1412
+ );
1413
+ const activeChatFirstApp = useMemo(
1414
+ () =>
1415
+ activeChatFirstAppRegistration
1416
+ ? {
1417
+ id: activeChatFirstAppRegistration.id,
1418
+ name:
1419
+ activeChatFirstAppRegistration.name ??
1420
+ activeChatFirstAppRegistration.id,
1421
+ }
1422
+ : null,
1423
+ [activeChatFirstAppRegistration],
1378
1424
  );
1379
1425
  const chatFirstAgentActivities = useMemo<ChatFirstAgentActivity[]>(
1380
1426
  () =>
@@ -1392,7 +1438,8 @@ export function Layout({
1392
1438
  activeChatFirstSurfaceTab?.kind === "app"
1393
1439
  ? (activeChatFirstSurfaceTab.path ??
1394
1440
  chatFirstPane?.path ??
1395
- activeChatFirstApp?.path)
1441
+ activeChatFirstAppRegistration?.path ??
1442
+ "/")
1396
1443
  : null;
1397
1444
  useEffect(() => {
1398
1445
  if (!chatFirstMode || !isChatRoute || !activeChatFirstApp) {
@@ -1454,6 +1501,12 @@ export function Layout({
1454
1501
  (pane: DispatchChatFirstPane) => {
1455
1502
  setChatFirstNotice(null);
1456
1503
  closeChatFirstSessionWatch();
1504
+ if (!isChatRoute) {
1505
+ navigateWithAgentChatViewTransition(
1506
+ navigate,
1507
+ dispatchNavLinkTarget("/chat"),
1508
+ );
1509
+ }
1457
1510
  const app = chatFirstAppRegistrations.find(
1458
1511
  (candidate) => candidate.id === pane.appId,
1459
1512
  );
@@ -1468,12 +1521,16 @@ export function Layout({
1468
1521
  ...(pane.path ? { path: pane.path } : {}),
1469
1522
  ...(pane.view ? { view: pane.view } : {}),
1470
1523
  });
1524
+ setChatFirstSurfacePanelOpen(true);
1471
1525
  persistChatFirstPane(pane);
1472
1526
  },
1473
1527
  [
1474
1528
  chatFirstAppRegistrations,
1475
1529
  chatFirstSurfaceTabsStore,
1476
1530
  persistChatFirstPane,
1531
+ isChatRoute,
1532
+ navigate,
1533
+ setChatFirstSurfacePanelOpen,
1477
1534
  ],
1478
1535
  );
1479
1536
  const openChatFirstSurface = useCallback(
@@ -1512,7 +1569,7 @@ export function Layout({
1512
1569
  );
1513
1570
  const resolveChatFirstOpenApp = useCallback(
1514
1571
  (detail: ChatFirstOpenAppDetail) => {
1515
- if (chatFirstAppsQuery.isLoading) {
1572
+ if (chatFirstAppsQuery.isLoading || chatFirstGrantedAppsQuery.isLoading) {
1516
1573
  pendingChatFirstOpenAppRef.current = detail;
1517
1574
  return;
1518
1575
  }
@@ -1543,11 +1600,10 @@ export function Layout({
1543
1600
  chatFirstAppRegistrations,
1544
1601
  chatFirstAppsQuery.isError,
1545
1602
  chatFirstAppsQuery.isLoading,
1603
+ chatFirstGrantedAppsQuery.isLoading,
1546
1604
  openChatFirstPane,
1547
1605
  ],
1548
1606
  );
1549
- const sidebarBeforeAppRef = useRef<boolean | null>(null);
1550
- const sidebarAutoCollapsedRef = useRef(false);
1551
1607
  const chatHomeHandoffActive = useAgentChatHomeHandoff({
1552
1608
  storageKey: "dispatch",
1553
1609
  activePath: localPathname,
@@ -1618,7 +1674,7 @@ export function Layout({
1618
1674
  );
1619
1675
 
1620
1676
  useEffect(() => {
1621
- if (!chatFirstMode || !isChatRoute) {
1677
+ if (!chatFirstMode) {
1622
1678
  setChatFirstPane(null);
1623
1679
  closeChatFirstSessionWatch();
1624
1680
  chatFirstSurfaceTabsStore.closeAll();
@@ -1721,7 +1777,7 @@ export function Layout({
1721
1777
  useEffect(() => {
1722
1778
  const tabCount = chatFirstSurfaceTabs.tabs.length;
1723
1779
  const previousTabCount = previousChatFirstSurfaceTabCountRef.current;
1724
- if (!chatFirstMode || !isChatRoute) {
1780
+ if (!chatFirstMode) {
1725
1781
  setChatFirstSurfacePanelOpen(false);
1726
1782
  } else if (
1727
1783
  tabCount > 0 &&
@@ -1756,12 +1812,19 @@ export function Layout({
1756
1812
 
1757
1813
  useEffect(() => {
1758
1814
  const pending = pendingChatFirstOpenAppRef.current;
1759
- if (!pending || chatFirstAppsQuery.isLoading) return;
1815
+ if (
1816
+ !pending ||
1817
+ chatFirstAppsQuery.isLoading ||
1818
+ chatFirstGrantedAppsQuery.isLoading
1819
+ )
1820
+ return;
1760
1821
  resolveChatFirstOpenApp(pending);
1761
1822
  }, [
1762
1823
  chatFirstAppsQuery.data,
1763
1824
  chatFirstAppsQuery.isError,
1764
1825
  chatFirstAppsQuery.isLoading,
1826
+ chatFirstGrantedAppsQuery.data,
1827
+ chatFirstGrantedAppsQuery.isLoading,
1765
1828
  resolveChatFirstOpenApp,
1766
1829
  ]);
1767
1830
 
@@ -1782,28 +1845,6 @@ export function Layout({
1782
1845
  isChatRoute,
1783
1846
  ]);
1784
1847
 
1785
- useEffect(() => {
1786
- if (isWorkspaceAppHostRoute) {
1787
- if (!sidebarAutoCollapsedRef.current) {
1788
- sidebarAutoCollapsedRef.current = true;
1789
- sidebarBeforeAppRef.current = sidebarCollapsed;
1790
- }
1791
- if (!sidebarCollapsed) setSidebarCollapsed(true);
1792
- return;
1793
- }
1794
-
1795
- if (!sidebarAutoCollapsedRef.current) return;
1796
- sidebarAutoCollapsedRef.current = false;
1797
- const previousSidebarState = sidebarBeforeAppRef.current;
1798
- sidebarBeforeAppRef.current = null;
1799
- if (
1800
- previousSidebarState !== null &&
1801
- previousSidebarState !== sidebarCollapsed
1802
- ) {
1803
- setSidebarCollapsed(previousSidebarState);
1804
- }
1805
- }, [isWorkspaceAppHostRoute, sidebarCollapsed]);
1806
-
1807
1848
  useEffect(() => {
1808
1849
  if (typeof window === "undefined" || isWorkspaceAppHostRoute) return;
1809
1850
  try {
@@ -1900,11 +1941,17 @@ export function Layout({
1900
1941
  (tab: ChatFirstSurfaceTab) => {
1901
1942
  if (tab.kind === "app") {
1902
1943
  if (tab.id !== chatFirstSurfaceTabs.activeTabId) return null;
1903
- const app = tab.appId
1904
- ? (chatFirstWorkspaceApps.find(
1944
+ const registration = tab.appId
1945
+ ? (chatFirstAppRegistrations.find(
1905
1946
  (candidate) => candidate.id === tab.appId,
1906
1947
  ) ?? null)
1907
1948
  : null;
1949
+ const app = registration
1950
+ ? {
1951
+ id: registration.id,
1952
+ name: registration.name ?? registration.id,
1953
+ }
1954
+ : null;
1908
1955
  return (
1909
1956
  <ChatFirstAppPane
1910
1957
  app={app}
@@ -2007,7 +2054,7 @@ export function Layout({
2007
2054
  chatFirstEmbedUrl,
2008
2055
  chatFirstSessionWatch.target,
2009
2056
  chatFirstSurfaceTabs.activeTabId,
2010
- chatFirstWorkspaceApps,
2057
+ chatFirstAppRegistrations,
2011
2058
  closeChatFirstSurfaceTab,
2012
2059
  renderChatFirstWatchChat,
2013
2060
  ],
@@ -2075,7 +2122,7 @@ export function Layout({
2075
2122
  <main
2076
2123
  className={cn(
2077
2124
  "flex-1",
2078
- isChatRoute || isWorkspaceAppHostRoute
2125
+ isChatRoute || isWorkspaceAppRoute
2079
2126
  ? "min-h-0 overflow-hidden"
2080
2127
  : "overflow-y-auto",
2081
2128
  )}
@@ -2090,6 +2137,40 @@ export function Layout({
2090
2137
  </main>
2091
2138
  </div>
2092
2139
  );
2140
+ const chatFirstSurfaceTabsBar =
2141
+ activeChatFirstSurfaceTab?.kind === "app" ? null : (
2142
+ <ChatFirstSurfaceTabs
2143
+ tabs={chatFirstSurfaceTabs.tabs}
2144
+ activeTabId={chatFirstSurfaceTabs.activeTabId}
2145
+ onActivate={activateChatFirstSurfaceTab}
2146
+ onClose={closeChatFirstSurfaceTab}
2147
+ onCloseOthers={(tab) => {
2148
+ activateChatFirstSurfaceTab(tab);
2149
+ chatFirstSurfaceTabsStore.closeOthers(tab.id);
2150
+ }}
2151
+ onCloseToRight={(tab) => {
2152
+ const targetIndex = chatFirstSurfaceTabs.tabs.findIndex(
2153
+ (candidate) => candidate.id === tab.id,
2154
+ );
2155
+ const activeIndex = chatFirstSurfaceTabs.tabs.findIndex(
2156
+ (candidate) => candidate.id === chatFirstSurfaceTabs.activeTabId,
2157
+ );
2158
+ if (activeIndex > targetIndex) activateChatFirstSurfaceTab(tab);
2159
+ chatFirstSurfaceTabsStore.closeToRight(tab.id);
2160
+ }}
2161
+ onCloseAll={closeAllChatFirstSurfaceTabs}
2162
+ onOpenSurface={openChatFirstSurface}
2163
+ copy={chatFirstCopy}
2164
+ />
2165
+ );
2166
+ const chatFirstSurfaceContent =
2167
+ chatFirstSurfaceTabs.tabs.length > 0 ? (
2168
+ <ChatFirstSurfaceContent
2169
+ tabs={chatFirstSurfaceTabs.tabs}
2170
+ activeTabId={chatFirstSurfaceTabs.activeTabId}
2171
+ renderTab={renderChatFirstSurfaceTab}
2172
+ />
2173
+ ) : null;
2093
2174
  const content = isChatRoute ? (
2094
2175
  <div
2095
2176
  className={cn(
@@ -2097,47 +2178,31 @@ export function Layout({
2097
2178
  chatFirstMode && "dispatch-chat-first-surface",
2098
2179
  )}
2099
2180
  >
2100
- {appContent}
2101
- {chatFirstMode && chatFirstSurfacePanel.open ? (
2102
- <ChatFirstSurfacePanel
2103
- width={chatFirstSurfaceResize.width}
2104
- onResizePointerDown={chatFirstSurfaceResize.onPointerDown}
2105
- copy={chatFirstCopy}
2181
+ {chatFirstAppTakesMain ? (
2182
+ <div
2183
+ className="flex min-w-0 flex-1 flex-col overflow-hidden"
2184
+ data-dispatch-chat-first-main-app
2106
2185
  >
2107
- <ChatFirstSurfaceTabs
2108
- tabs={chatFirstSurfaceTabs.tabs}
2109
- activeTabId={chatFirstSurfaceTabs.activeTabId}
2110
- onActivate={activateChatFirstSurfaceTab}
2111
- onClose={closeChatFirstSurfaceTab}
2112
- onCloseOthers={(tab) => {
2113
- activateChatFirstSurfaceTab(tab);
2114
- chatFirstSurfaceTabsStore.closeOthers(tab.id);
2115
- }}
2116
- onCloseToRight={(tab) => {
2117
- const targetIndex = chatFirstSurfaceTabs.tabs.findIndex(
2118
- (candidate) => candidate.id === tab.id,
2119
- );
2120
- const activeIndex = chatFirstSurfaceTabs.tabs.findIndex(
2121
- (candidate) =>
2122
- candidate.id === chatFirstSurfaceTabs.activeTabId,
2123
- );
2124
- if (activeIndex > targetIndex) activateChatFirstSurfaceTab(tab);
2125
- chatFirstSurfaceTabsStore.closeToRight(tab.id);
2126
- }}
2127
- onCloseAll={closeAllChatFirstSurfaceTabs}
2128
- onOpenSurface={openChatFirstSurface}
2129
- copy={chatFirstCopy}
2130
- />
2131
- {chatFirstSurfaceTabs.tabs.length > 0 ? (
2132
- <ChatFirstSurfaceContent
2133
- tabs={chatFirstSurfaceTabs.tabs}
2134
- activeTabId={chatFirstSurfaceTabs.activeTabId}
2135
- renderTab={renderChatFirstSurfaceTab}
2136
- />
2186
+ {chatFirstSurfaceContent}
2187
+ </div>
2188
+ ) : (
2189
+ <>
2190
+ {appContent}
2191
+ {chatFirstMode && chatFirstSurfacePanel.open ? (
2192
+ <ChatFirstSurfacePanel
2193
+ width={chatFirstSurfaceResize.width}
2194
+ onResizePointerDown={chatFirstSurfaceResize.onPointerDown}
2195
+ copy={chatFirstCopy}
2196
+ >
2197
+ {chatFirstSurfaceTabsBar}
2198
+ {chatFirstSurfaceContent}
2199
+ </ChatFirstSurfacePanel>
2137
2200
  ) : null}
2138
- </ChatFirstSurfacePanel>
2139
- ) : null}
2201
+ </>
2202
+ )}
2140
2203
  </div>
2204
+ ) : isWorkspaceAppRoute ? (
2205
+ appContent
2141
2206
  ) : (
2142
2207
  <AgentSidebar
2143
2208
  position="right"
@@ -97,6 +97,7 @@ export function WorkspaceAppsRail({
97
97
  id={app.id}
98
98
  name={label}
99
99
  size="sm"
100
+ monochrome
100
101
  className={cn("size-5 rounded-md", active && "ring-1 ring-ring/30")}
101
102
  />
102
103
  {!collapsed ? <span className="truncate">{label}</span> : null}
@@ -1,5 +1,4 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
- import { IconStack2 } from "@tabler/icons-react";
3
2
 
4
3
  import { filterOtherApps, type ConnectedAppSummary } from "../lib/other-apps";
5
4
  import type { WorkspaceAppId } from "../lib/other-apps";
@@ -80,6 +79,8 @@ export function OtherAppsSection({
80
79
  onRetryConnectedApps,
81
80
  templateLabels,
82
81
  onRemixSuccess,
82
+ heading = "Other apps",
83
+ embeddedInList = false,
83
84
  className,
84
85
  }: {
85
86
  templates?: CuratedWorkspaceTemplatesResult;
@@ -96,6 +97,8 @@ export function OtherAppsSection({
96
97
  result: unknown,
97
98
  template: CuratedWorkspaceTemplate,
98
99
  ) => void;
100
+ heading?: string | null;
101
+ embeddedInList?: boolean;
99
102
  className?: string;
100
103
  }) {
101
104
  const t = useT();
@@ -109,26 +112,15 @@ export function OtherAppsSection({
109
112
 
110
113
  if (!isLoading && !hasError && entries.length === 0) return null;
111
114
 
112
- return (
113
- <section className={cn("space-y-3 border-t pt-6", className)}>
114
- <div className="flex min-w-0 items-start gap-2">
115
- <IconStack2
116
- size={16}
117
- className="mt-0.5 shrink-0 text-muted-foreground"
118
- />
119
- <div className="min-w-0">
115
+ const content = (
116
+ <>
117
+ {heading ? (
118
+ <div className="flex min-w-0 items-center">
120
119
  <h2 className="truncate text-sm font-semibold text-foreground">
121
- {t("dispatch.pages.otherApps", { defaultValue: "Other apps" })}
120
+ {t("dispatch.pages.otherApps", { defaultValue: heading })}
122
121
  </h2>
123
- {entries.length > 0 ? (
124
- <p className="mt-0.5 text-xs text-muted-foreground">
125
- {t("dispatch.pages.availableCount", {
126
- count: entries.length,
127
- })}
128
- </p>
129
- ) : null}
130
122
  </div>
131
- </div>
123
+ ) : null}
132
124
 
133
125
  {templatesError ? (
134
126
  <ActionQueryError
@@ -144,31 +136,37 @@ export function OtherAppsSection({
144
136
  ) : null}
145
137
 
146
138
  {isLoading && entries.length === 0 ? (
147
- <OtherAppsSkeletonList />
139
+ embeddedInList ? (
140
+ <OtherAppsSkeletonRows />
141
+ ) : (
142
+ <OtherAppsSkeletonList />
143
+ )
148
144
  ) : entries.length > 0 ? (
149
- <AppList className={APP_LIST_GRID_CLASS}>
150
- {entries.map((entry) =>
151
- entry.kind === "template" ? (
152
- <WorkspaceTemplateCard
153
- key={`template:${templateKey(entry.template)}`}
154
- template={entry.template}
155
- labels={templateLabels}
156
- catalog
157
- className={APP_LIST_GRID_ROW_CLASS}
158
- onRemixSuccess={onRemixSuccess}
159
- />
160
- ) : (
161
- <ConnectedAppCard
162
- key={`connected:${entry.app.id}`}
163
- app={entry.app}
164
- className={APP_LIST_GRID_ROW_CLASS}
165
- />
166
- ),
167
- )}
168
- </AppList>
145
+ entries.map((entry) =>
146
+ entry.kind === "template" ? (
147
+ <WorkspaceTemplateCard
148
+ key={`template:${templateKey(entry.template)}`}
149
+ template={entry.template}
150
+ labels={templateLabels}
151
+ catalog
152
+ className={APP_LIST_GRID_ROW_CLASS}
153
+ onRemixSuccess={onRemixSuccess}
154
+ />
155
+ ) : (
156
+ <ConnectedAppCard
157
+ key={`connected:${entry.app.id}`}
158
+ app={entry.app}
159
+ className={APP_LIST_GRID_ROW_CLASS}
160
+ />
161
+ ),
162
+ )
169
163
  ) : null}
170
- </section>
164
+ </>
171
165
  );
166
+
167
+ if (embeddedInList) return content;
168
+
169
+ return <section className={cn("space-y-3", className)}>{content}</section>;
172
170
  }
173
171
 
174
172
  function OtherAppsSkeletonList() {
@@ -193,3 +191,26 @@ function OtherAppsSkeletonList() {
193
191
  </AppList>
194
192
  );
195
193
  }
194
+
195
+ function OtherAppsSkeletonRows() {
196
+ return (
197
+ <>
198
+ {Array.from({ length: 4 }).map((_, index) => (
199
+ <div
200
+ key={index}
201
+ className={cn(
202
+ "flex min-w-0 items-center gap-3 border-b px-4 py-3.5 last:border-b-0",
203
+ APP_LIST_GRID_ROW_CLASS,
204
+ )}
205
+ >
206
+ <Skeleton className="size-8 shrink-0 rounded-xl" />
207
+ <div className="min-w-0 flex-1 space-y-2">
208
+ <Skeleton className="h-4 w-32" />
209
+ <Skeleton className="h-3 w-2/3" />
210
+ </div>
211
+ <Skeleton className="h-9 w-24 shrink-0 rounded-md" />
212
+ </div>
213
+ ))}
214
+ </>
215
+ );
216
+ }