@agent-native/core 0.217.0 → 0.218.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.
@@ -139,8 +139,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
139
139
  duplicate?: undefined;
140
140
  eventId?: undefined;
141
141
  } | {
142
+ error?: undefined;
142
143
  accepted: boolean;
143
144
  duplicate: boolean;
144
145
  eventId: string;
145
- error?: undefined;
146
146
  }>;
@@ -19,7 +19,7 @@ export { AGENT_NATIVE_MCP_APP_HOST_MESSAGE_TYPES, getMcpAppHostContext, initiali
19
19
  export { buildDynamicAgentSuggestions, dedupeSuggestions, mergeAgentSuggestions, normalizeAgentDynamicSuggestionsConfig, useAgentDynamicSuggestions, useAgentDynamicSuggestionsResult, type AgentDynamicSuggestionContext, type AgentDynamicSuggestionsConfig, type AgentDynamicSuggestionsOption, } from "../dynamic-suggestions.js";
20
20
  export { clearChatStorage } from "../chat/storage.js";
21
21
  export { isAssistantChatHistoryVersion, coerceAssistantChatHistoryDate, type AssistantChatHistoryDate, type AssistantChatHistoryVersion, } from "../chat/assistant-chat-history-version.js";
22
- export { ChatThreadListConfigError, useChatThreadList, type UseChatThreadListOptions, type UseChatThreadListResult, } from "../use-chat-thread-list.js";
22
+ export { ChatThreadListConfigError, useChatThreadList, type UseChatThreadListOptions, type UseChatThreadListResult, type ChatThreadListRefreshOutcome, } from "../use-chat-thread-list.js";
23
23
  export { ChatThreadListError, fetchChatThreadListPage, notifyChatThreadsUpdated, useChatThreads, type ChatThreadAccessBasis, type ChatThreadListPage, type ChatThreadListRow, type ChatThreadListPageRequest, type ChatThreadOwnership, type ChatThreadVisibility, type ChatThreadScope, type ChatThreadSnapshot, type ChatThreadSummary, type ChatThreadData, type ChatThreadShareLink, type ChatThreadShareState, type UseChatThreadsOptions, } from "../use-chat-threads.js";
24
24
  export { type AgentCallProgress, type ContentPart, } from "../sse-event-processor.js";
25
25
  export { useRunStuckDetection, useAbortRun, type RunStuckState, type UseRunStuckDetectionOptions, } from "../use-run-stuck-detection.js";
@@ -16,6 +16,7 @@ export interface UseChatThreadListOptions {
16
16
  refreshOnThreadUpdates?: boolean;
17
17
  apiUrl?: string;
18
18
  }
19
+ export type ChatThreadListRefreshOutcome = "success" | "error" | "superseded" | "disabled";
19
20
  export interface UseChatThreadListResult {
20
21
  threads: ChatThreadListRow[];
21
22
  /** True until the first page of the current list settles. */
@@ -27,6 +28,8 @@ export interface UseChatThreadListResult {
27
28
  hasMore: boolean;
28
29
  loadMore: () => Promise<void>;
29
30
  refresh: () => Promise<void>;
31
+ /** Reports whether this invocation applied the complete refreshed window. */
32
+ refreshWithResult: () => Promise<ChatThreadListRefreshOutcome>;
30
33
  }
31
34
  /** `pageSize` is not an integer between 1 and the server's page cap. */
32
35
  export declare class ChatThreadListConfigError extends Error {
@@ -116,7 +116,7 @@ export function useChatThreadList(options) {
116
116
  // rows and offset already on screen untouched.
117
117
  const replace = useCallback(async (window) => {
118
118
  if (!session.active)
119
- return;
119
+ return "superseded";
120
120
  const requestId = ++session.requestId;
121
121
  const isCurrent = () => session.active && requestId === session.requestId;
122
122
  session.inFlight = "replace";
@@ -131,7 +131,7 @@ export function useChatThreadList(options) {
131
131
  limit: Math.min(CHAT_THREADS_MAX_PAGE_SIZE, window - consumed),
132
132
  });
133
133
  if (!isCurrent())
134
- return;
134
+ return "superseded";
135
135
  for (const thread of page.threads) {
136
136
  if (!rows.has(thread.id))
137
137
  rows.set(thread.id, thread);
@@ -142,11 +142,13 @@ export function useChatThreadList(options) {
142
142
  session.nextOffset = consumed;
143
143
  session.hasMore = more;
144
144
  update({ threads: [...rows.values()], hasMore: more, error: null });
145
+ return "success";
145
146
  }
146
147
  catch (failure) {
147
148
  if (!isCurrent())
148
- return;
149
+ return "superseded";
149
150
  update({ error: asError(failure) });
151
+ return "error";
150
152
  }
151
153
  finally {
152
154
  if (isCurrent()) {
@@ -155,13 +157,18 @@ export function useChatThreadList(options) {
155
157
  }
156
158
  }
157
159
  }, [request, session, update]);
158
- const refresh = useCallback(async () => {
160
+ const refreshWithResult = useCallback(async () => {
161
+ if (!session.active)
162
+ return "superseded";
159
163
  if (!enabled)
160
- return;
164
+ return "disabled";
161
165
  // Reload every row already on screen so a mutation does not collapse a
162
166
  // list the person has paged through.
163
- await replace(Math.max(pageSize, session.nextOffset));
167
+ return replace(Math.max(pageSize, session.nextOffset));
164
168
  }, [enabled, pageSize, replace, session]);
169
+ const refresh = useCallback(async () => {
170
+ await refreshWithResult();
171
+ }, [refreshWithResult]);
165
172
  const loadMore = useCallback(async () => {
166
173
  if (!session.active || session.inFlight || !session.hasMore)
167
174
  return;
@@ -216,5 +223,14 @@ export function useChatThreadList(options) {
216
223
  return () => window.removeEventListener(CHAT_THREADS_UPDATED_EVENT, onUpdated);
217
224
  }, [enabled, refresh, refreshOnThreadUpdates]);
218
225
  const { threads, loading, loadingMore, error, hasMore } = state;
219
- return { threads, loading, loadingMore, error, hasMore, loadMore, refresh };
226
+ return {
227
+ threads,
228
+ loading,
229
+ loadingMore,
230
+ error,
231
+ hasMore,
232
+ loadMore,
233
+ refresh,
234
+ refreshWithResult,
235
+ };
220
236
  }
@@ -16,9 +16,9 @@ export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import
16
16
  error: string;
17
17
  ok?: undefined;
18
18
  } | {
19
+ error?: undefined;
19
20
  liveCopy: "derived" | "not_derived" | "unchanged";
20
21
  liveCopyReason?: string;
21
- error?: undefined;
22
22
  ok: boolean;
23
23
  } | {
24
24
  error?: undefined;
@@ -1,6 +1,6 @@
1
1
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
2
- error: string;
3
2
  ok?: undefined;
3
+ error: string;
4
4
  } | {
5
5
  error?: undefined;
6
6
  ok: boolean;
@@ -13,10 +13,10 @@ declare const _default: import("../../action.js").ActionDefinition<{
13
13
  enabled?: undefined;
14
14
  nextRun?: undefined;
15
15
  } | {
16
+ deleted?: undefined;
16
17
  resourceId: string;
17
18
  name: string;
18
19
  enabled: boolean;
19
20
  nextRun: string;
20
- deleted?: undefined;
21
21
  }>;
22
22
  export default _default;
@@ -1,13 +1,13 @@
1
1
  export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
2
2
  count: number;
3
3
  updated?: undefined;
4
- ok?: undefined;
5
4
  error?: undefined;
5
+ ok?: undefined;
6
6
  } | {
7
7
  count?: undefined;
8
8
  updated: number;
9
- ok?: undefined;
10
9
  error?: undefined;
10
+ ok?: undefined;
11
11
  } | {
12
12
  count?: undefined;
13
13
  updated?: undefined;
@@ -16,6 +16,6 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
16
16
  } | {
17
17
  count?: undefined;
18
18
  updated?: undefined;
19
- ok: boolean;
20
19
  error?: undefined;
20
+ ok: boolean;
21
21
  }>>;
@@ -33,27 +33,27 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
33
33
  summary?: undefined;
34
34
  spans?: undefined;
35
35
  } | {
36
+ error?: undefined;
36
37
  ok?: undefined;
37
38
  message?: undefined;
38
39
  summary: import("./types.js").TraceSummary;
39
40
  spans: import("./types.js").TraceSpan[];
40
- error?: undefined;
41
41
  } | {
42
+ error?: undefined;
42
43
  ok?: undefined;
43
44
  message?: undefined;
44
45
  summary?: undefined;
45
46
  spans?: undefined;
46
47
  traceMissing: true;
47
48
  id: string;
48
- error?: undefined;
49
49
  } | {
50
+ error?: undefined;
50
51
  ok?: undefined;
51
52
  message?: undefined;
52
53
  summary?: undefined;
53
54
  spans?: undefined;
54
55
  traceMissing?: undefined;
55
56
  id: string;
56
- error?: undefined;
57
57
  } | {
58
58
  ok?: undefined;
59
59
  message?: undefined;
@@ -61,9 +61,9 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
61
61
  spans?: undefined;
62
62
  error: any;
63
63
  } | {
64
+ error?: undefined;
64
65
  message?: undefined;
65
66
  summary?: undefined;
66
67
  spans?: undefined;
67
68
  ok: boolean;
68
- error?: undefined;
69
69
  }>>;
@@ -57,39 +57,39 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
57
57
  } | {
58
58
  error: string;
59
59
  errorCode?: undefined;
60
- status?: undefined;
61
60
  ok?: undefined;
61
+ status?: undefined;
62
62
  } | {
63
63
  error: string;
64
64
  errorCode: string;
65
- status?: undefined;
66
65
  ok?: undefined;
66
+ status?: undefined;
67
67
  } | {
68
+ error?: undefined;
68
69
  errorCode?: undefined;
69
70
  ok: boolean;
70
71
  status: string;
71
- error?: undefined;
72
72
  } | {
73
+ ok?: undefined;
73
74
  error: string;
74
75
  removed?: undefined;
75
- ok?: undefined;
76
76
  } | {
77
+ error?: undefined;
77
78
  ok: boolean;
78
79
  removed: boolean;
79
- error?: undefined;
80
80
  }>>;
81
81
  export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
82
+ ok?: undefined;
82
83
  error: string;
83
84
  note?: undefined;
84
- ok?: undefined;
85
85
  } | {
86
+ error?: undefined;
86
87
  ok: boolean;
87
88
  note?: undefined;
88
- error?: undefined;
89
89
  } | {
90
+ error?: undefined;
90
91
  ok: boolean;
91
92
  note: string;
92
- error?: undefined;
93
93
  } | {
94
94
  note?: undefined;
95
95
  ok: boolean;
@@ -117,11 +117,11 @@ export declare function createAdHocSecretHandler(): import("h3").EventHandlerWit
117
117
  } | {
118
118
  error: string;
119
119
  } | {
120
+ error?: undefined;
120
121
  ok: boolean;
121
122
  key: string;
122
- error?: undefined;
123
123
  } | {
124
+ error?: undefined;
124
125
  ok: boolean;
125
126
  removed: boolean;
126
- error?: undefined;
127
127
  }>>;
@@ -67,12 +67,12 @@ export declare function createAgentEngineApiKeyHandler(): import("h3").EventHand
67
67
  code: any;
68
68
  } | {
69
69
  errorCode?: undefined;
70
+ error?: undefined;
70
71
  code?: undefined;
71
72
  ok: boolean;
72
73
  key: string;
73
74
  baseUrlKey?: string;
74
75
  scope: AgentEngineApiKeyScope;
75
76
  defaultModel?: import("./agent-engine-default-model-route.js").SavedKeyDefaultModelOutcome;
76
- error?: undefined;
77
77
  }>>;
78
78
  export {};
@@ -16,9 +16,9 @@ export declare function createAgentEngineDisconnectHandler(): import("h3").Event
16
16
  error: any;
17
17
  scope?: undefined;
18
18
  } | {
19
- error?: undefined;
20
19
  ok: boolean;
21
20
  scope: "org" | "user";
21
+ error?: undefined;
22
22
  }>>;
23
23
  export interface DefaultModelSelectionRequest {
24
24
  engine: string;
@@ -1,9 +1,9 @@
1
1
  export declare function createAgentEngineOllamaModelsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
2
+ ok?: undefined;
2
3
  error: string;
3
4
  models?: undefined;
4
- ok?: undefined;
5
5
  } | {
6
+ error?: undefined;
6
7
  ok: boolean;
7
8
  models: string[];
8
- error?: undefined;
9
9
  }>>;
@@ -588,8 +588,8 @@ export declare function createPublicRemoteAgentsHandler(discover?: PublicAgentDi
588
588
  error: string;
589
589
  agents?: undefined;
590
590
  } | {
591
- agents: Omit<import("./agent-discovery.js").DiscoveredAgent, "auth" | "kind">[];
592
591
  error?: undefined;
592
+ agents: Omit<import("./agent-discovery.js").DiscoveredAgent, "auth" | "kind">[];
593
593
  }>>;
594
594
  export declare function getBuilderConnectErrorDisposition(error: unknown, connectAttemptId: string | null): "correlated" | "legacy" | null;
595
595
  export declare function getBuilderConnectErrorKey(ownerEmail: string, connectAttemptId?: string | null): string;
@@ -54,8 +54,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
54
54
  expiresAt?: undefined;
55
55
  ttlSeconds?: undefined;
56
56
  } | {
57
- error?: undefined;
58
57
  token: string;
59
58
  expiresAt: string;
60
59
  ttlSeconds: number;
60
+ error?: undefined;
61
61
  }>>;
@@ -9,6 +9,7 @@
9
9
  "app/components/layout/AgentInspector.test.tsx",
10
10
  "app/components/layout/AgentInspector.tsx",
11
11
  "app/components/layout/Header.tsx",
12
+ "app/components/layout/Sidebar.spec.tsx",
12
13
  "app/components/layout/Sidebar.tsx",
13
14
  "app/i18n",
14
15
  "app/i18n-data.ts",
@@ -1,3 +1,4 @@
1
+ import { appPath } from "@agent-native/core/client/api-path";
1
2
  import { useT } from "@agent-native/core/client/i18n";
2
3
  import {
3
4
  navigateWithAgentChatViewTransition,
@@ -5,14 +6,15 @@ import {
5
6
  useChatThreads,
6
7
  type ChatThreadSummary,
7
8
  } from "@agent-native/toolkit/app/chat/agentkit-chat/rail";
9
+ import {
10
+ ownDraftRows,
11
+ OwnershipChatHistoryRail,
12
+ useOwnershipChatHistory,
13
+ type OwnDraftRow,
14
+ } from "@agent-native/toolkit/app/chat/ownership-history";
8
15
  import { OrgSwitcher } from "@agent-native/toolkit/app/org";
9
16
  import { openCommandMenu } from "@agent-native/toolkit/app/shared";
10
17
  import { AgentNativeIcon } from "@agent-native/toolkit/app/shared/AgentNativeIcon";
11
- import {
12
- ChatHistoryList,
13
- type ChatHistoryItem,
14
- type ChatHistorySection,
15
- } from "@agent-native/toolkit/chat-history/ChatHistoryList";
16
18
  import {
17
19
  IconApps,
18
20
  IconClock,
@@ -25,7 +27,7 @@ import {
25
27
  IconPin,
26
28
  IconSearch,
27
29
  } from "@tabler/icons-react";
28
- import { useEffect, useMemo, useRef, useState } from "react";
30
+ import { useEffect, useRef, useState } from "react";
29
31
  import { Link, useLocation, useNavigate } from "react-router";
30
32
  import { toast } from "sonner";
31
33
 
@@ -41,7 +43,6 @@ import {
41
43
  } from "@/components/ui/tooltip";
42
44
  import { APP_TITLE } from "@/lib/app-config";
43
45
  import { chatThreadPath, HOME_PATH, threadIdFromPath } from "@/lib/chat-paths";
44
- import { visibleChatThreads } from "@/lib/sidebar-thread-state";
45
46
  import { cn } from "@/lib/utils";
46
47
 
47
48
  const CHAT_STORAGE_KEY = "chat";
@@ -53,11 +54,9 @@ interface SidebarProps {
53
54
  onCollapsedChange?: (collapsed: boolean) => void;
54
55
  }
55
56
 
56
- function threadTitle(thread: ChatThreadSummary, untitledLabel: string) {
57
- return thread.title || untitledLabel;
58
- }
59
-
60
- function threadUpdatedAt(thread: ChatThreadSummary) {
57
+ function threadUpdatedAt(
58
+ thread: Pick<ChatThreadSummary, "createdAt" | "updatedAt">,
59
+ ) {
61
60
  return Number.isFinite(thread.updatedAt)
62
61
  ? thread.updatedAt
63
62
  : Number.isFinite(thread.createdAt)
@@ -65,14 +64,6 @@ function threadUpdatedAt(thread: ChatThreadSummary) {
65
64
  : 0;
66
65
  }
67
66
 
68
- function compareRecentThreads(a: ChatThreadSummary, b: ChatThreadSummary) {
69
- return threadUpdatedAt(b) - threadUpdatedAt(a);
70
- }
71
-
72
- function comparePinnedThreads(a: ChatThreadSummary, b: ChatThreadSummary) {
73
- return (b.pinnedAt ?? 0) - (a.pinnedAt ?? 0);
74
- }
75
-
76
67
  function formatRelativeTime(timestamp: number) {
77
68
  const elapsed = timestamp - Date.now();
78
69
  const intervals = [
@@ -106,7 +97,10 @@ function ThreadHistoryTitle({
106
97
  title,
107
98
  running,
108
99
  }: {
109
- thread: ChatThreadSummary;
100
+ thread: OwnDraftRow &
101
+ Partial<
102
+ Pick<ChatThreadSummary, "preview" | "scope" | "source" | "pinnedAt">
103
+ >;
110
104
  title: string;
111
105
  running: boolean;
112
106
  }) {
@@ -287,7 +281,7 @@ function ChatThreadsSection({ collapsed }: { collapsed: boolean }) {
287
281
  pinThread,
288
282
  archiveThread,
289
283
  renameThread,
290
- refreshThreads,
284
+ isNewThread,
291
285
  } = useChatThreads(undefined, CHAT_STORAGE_KEY, undefined, {
292
286
  autoCreate: false,
293
287
  restoreActiveThread: false,
@@ -295,66 +289,46 @@ function ChatThreadsSection({ collapsed }: { collapsed: boolean }) {
295
289
  const { workingThreadIds, observedThreadStarts } =
296
290
  useAgentChatRunningThreads();
297
291
 
298
- const visibleThreads = useMemo(
299
- () => visibleChatThreads(threads, observedThreadStarts),
300
- [observedThreadStarts, threads],
301
- );
302
- const displayedActiveThreadId =
303
- threadIdFromPath(location.pathname) ??
304
- activeThreadId ??
305
- persistedActiveThreadId();
306
-
307
- const historySections = useMemo<ChatHistorySection[]>(() => {
308
- const untitledLabel = t("chat.untitledChat");
309
- const toItem = (thread: ChatThreadSummary): ChatHistoryItem => {
310
- const title = threadTitle(thread, untitledLabel);
292
+ const localMineRows = ownDraftRows({
293
+ threads,
294
+ isNewThread,
295
+ observedThreadStarts,
296
+ });
297
+ const history = useOwnershipChatHistory({
298
+ getThreadUrl: (threadId) => appPath(chatThreadPath(threadId)),
299
+ activeId:
300
+ threadIdFromPath(location.pathname) ??
301
+ activeThreadId ??
302
+ persistedActiveThreadId(),
303
+ localMineRows,
304
+ pinned: { label: t("chat.pinned") },
305
+ mutations: {
306
+ pin: pinThread,
307
+ rename: renameThread,
308
+ archive: archiveThread,
309
+ },
310
+ onMutationFailed: (kind) => {
311
+ if (kind === "archive") toast.error(t("chat.archiveFailed"));
312
+ if (kind === "rename") toast.error(t("chat.renameFailed"));
313
+ },
314
+ decorateItem: (item, row) => {
315
+ const title = row.title || row.preview || t("chat.untitledChat");
311
316
  return {
312
- id: thread.id,
317
+ ...item,
313
318
  title: (
314
319
  <ThreadHistoryTitle
315
- thread={thread}
320
+ thread={row}
316
321
  title={title}
317
- running={workingThreadIds.has(thread.id)}
322
+ running={workingThreadIds.has(row.id)}
318
323
  />
319
324
  ),
320
- titleText: title,
321
- pinned: Boolean(thread.pinnedAt),
322
325
  };
323
- };
324
- const pinned = visibleThreads
325
- .filter((thread) => Boolean(thread.pinnedAt))
326
- .sort(comparePinnedThreads)
327
- .map(toItem);
328
- const recent = visibleThreads
329
- .filter((thread) => !thread.pinnedAt)
330
- .sort(compareRecentThreads)
331
- .slice(0, 30)
332
- .map(toItem);
333
-
334
- return [
335
- { id: "pinned", label: t("chat.pinned"), items: pinned },
336
- { id: "recents", label: t("chat.recents"), items: recent },
337
- ];
338
- }, [t, visibleThreads, workingThreadIds]);
339
-
340
- useEffect(() => {
341
- const refresh = () => refreshThreads();
342
- const handleRunning = (event: Event) => {
343
- const detail = (event as CustomEvent).detail as
344
- | { isRunning?: unknown }
345
- | undefined;
346
- if (typeof detail?.isRunning === "boolean") refreshThreads();
347
- };
348
-
349
- window.addEventListener("agent-chat:threads-updated", refresh);
350
- window.addEventListener("agentNative.chatRunning", handleRunning);
351
- window.addEventListener("focus", refresh);
352
- return () => {
353
- window.removeEventListener("agent-chat:threads-updated", refresh);
354
- window.removeEventListener("agentNative.chatRunning", handleRunning);
355
- window.removeEventListener("focus", refresh);
356
- };
357
- }, [refreshThreads]);
326
+ },
327
+ });
328
+ const displayedActiveThreadId =
329
+ threadIdFromPath(location.pathname) ??
330
+ activeThreadId ??
331
+ persistedActiveThreadId();
358
332
 
359
333
  function openThread(threadId: string, options?: { isNew?: boolean }) {
360
334
  switchThread(threadId);
@@ -382,23 +356,6 @@ function ChatThreadsSection({ collapsed }: { collapsed: boolean }) {
382
356
  if (threadId) openThread(threadId, { isNew: true });
383
357
  }
384
358
 
385
- async function handleArchiveThread(threadId: string) {
386
- const wasActive =
387
- threadId === activeThreadId || threadId === persistedActiveThreadId();
388
- const archived = await archiveThread(threadId);
389
- if (!archived) {
390
- toast.error(t("chat.archiveFailed"));
391
- return;
392
- }
393
- if (wasActive) await handleNewChat();
394
- }
395
-
396
- function handleRenameThread(threadId: string, title: string) {
397
- void renameThread(threadId, title).then((renamed) => {
398
- if (!renamed) toast.error(t("chat.renameFailed"));
399
- });
400
- }
401
-
402
359
  const newChatButton = (
403
360
  <button
404
361
  type="button"
@@ -430,17 +387,20 @@ function ChatThreadsSection({ collapsed }: { collapsed: boolean }) {
430
387
  return (
431
388
  <div className="flex min-h-0 flex-1 flex-col">
432
389
  <div className="px-2 pb-3">{newChatButton}</div>
433
- <ChatHistoryList
434
- sections={historySections}
390
+ <OwnershipChatHistoryRail
391
+ {...history}
392
+ onNewChat={() => void handleNewChat()}
393
+ showNewChat={false}
435
394
  activeId={displayedActiveThreadId}
436
395
  onSelect={openThread}
437
- onTogglePin={(threadId) => {
438
- const thread = visibleThreads.find((item) => item.id === threadId);
439
- if (thread) void pinThread(threadId, !thread.pinnedAt);
396
+ onDelete={async (threadId) => {
397
+ const wasActive =
398
+ threadId === activeThreadId ||
399
+ threadId === persistedActiveThreadId();
400
+ const archived = await history.listProps.onDelete?.(threadId);
401
+ if (archived && wasActive) await handleNewChat();
440
402
  }}
441
- onRename={handleRenameThread}
442
403
  renameMaxLength={160}
443
- onDelete={(threadId) => void handleArchiveThread(threadId)}
444
404
  labels={{
445
405
  options: (item) =>
446
406
  t("chat.optionsFor", { title: item.titleText ?? "" }),
@@ -451,8 +411,9 @@ function ChatThreadsSection({ collapsed }: { collapsed: boolean }) {
451
411
  unpin: t("chat.unpinChat"),
452
412
  delete: t("chat.archiveChat"),
453
413
  }}
454
- variant="rail"
455
- emptyLabel={null}
414
+ railLabels={{
415
+ newChat: t("chat.newChat"),
416
+ }}
456
417
  className="chat-sidebar-history min-h-0 flex-1"
457
418
  listClassName="pb-4"
458
419
  />
@@ -67,6 +67,7 @@ declare const _default: import("../../action.js").ActionDefinition<{
67
67
  triggerType: "event" | "manual" | "schedule" | "webhook";
68
68
  body: string;
69
69
  }, {
70
+ deleted?: undefined;
70
71
  created: boolean;
71
72
  resourceId: string;
72
73
  name: string;
@@ -87,12 +88,13 @@ declare const _default: import("../../action.js").ActionDefinition<{
87
88
  executionCwd: string;
88
89
  mcpTools: string[];
89
90
  webhookPath: string;
90
- deleted?: undefined;
91
91
  updated?: undefined;
92
92
  changed?: undefined;
93
93
  previouslyEnabled?: undefined;
94
94
  requestedEnabled?: undefined;
95
95
  } | {
96
+ enabled?: undefined;
97
+ nextRun?: undefined;
96
98
  created?: undefined;
97
99
  deleted: boolean;
98
100
  resourceId: string;
@@ -105,8 +107,6 @@ declare const _default: import("../../action.js").ActionDefinition<{
105
107
  timezone?: undefined;
106
108
  condition?: undefined;
107
109
  body?: undefined;
108
- enabled?: undefined;
109
- nextRun?: undefined;
110
110
  createdBy?: undefined;
111
111
  model?: undefined;
112
112
  reasoningEffort?: undefined;
@@ -119,8 +119,8 @@ declare const _default: import("../../action.js").ActionDefinition<{
119
119
  requestedEnabled?: undefined;
120
120
  webhookPath?: undefined;
121
121
  } | {
122
- created?: undefined;
123
122
  deleted?: undefined;
123
+ created?: undefined;
124
124
  updated: boolean;
125
125
  resourceId: string;
126
126
  name: string;
@@ -696,24 +696,25 @@ function WorkspaceResources() {
696
696
 
697
697
  ## Other Public UI {#other-ui}
698
698
 
699
- | Area | APIs | Import path |
700
- | -------------- | ------------------------------------------------------------------------------ | ------------------------------------------ |
701
- | Agent page | `<AgentTabsPage>` | `@agent-native/toolkit/app/agent-page` |
702
- | Blocks UI | `BlockView`, `SchemaBlockEditor`, `BlockRegistryProvider` | `@agent-native/toolkit/app/blocks` |
703
- | Canvas | `DrawOverlay`, `CanvasCommentPins` | `@agent-native/toolkit/canvas-annotations` |
704
- | Chat history | `ChatHistoryList`, `ChatHistoryRail` | `@agent-native/toolkit/chat-history` |
705
- | Data grid | `<DataGrid>` | `@agent-native/toolkit/data-grid` |
706
- | Dashboard | `DataTable`, `DateRangePicker`, `GenericChartPanel`, `MetricCard`, `StatsCard` | `@agent-native/toolkit/dashboard` |
707
- | Sharing | `<ShareButton>`, `<ShareDialog>` | `@agent-native/toolkit/app/sharing` |
708
- | Sharing UI | `<VisibilityBadge>` | `@agent-native/toolkit/sharing` |
709
- | Notifications | `<NotificationsBell>` | `@agent-native/toolkit/app/notifications` |
710
- | Progress | `<RunsTray>` | `@agent-native/toolkit/app/progress` |
711
- | Run registry | Run types and server registry functions | `@agent-native/core/progress` |
712
- | Onboarding | `useOnboarding()`, onboarding panel hooks | `@agent-native/core/client/onboarding` |
713
- | Observability | `<ObservabilityDashboard>`, `<ThumbsFeedback>` | `@agent-native/toolkit/app/observability` |
714
- | Resources UI | `<ResourcesPanel>`, `<ResourceTree>`, `<ResourceEditor>` | `@agent-native/toolkit/app/resources` |
715
- | Resource hooks | Resource data hooks | `@agent-native/core/client/resources` |
716
- | Rich editor | `<SharedRichEditor>`, slash commands, block node hooks | `@agent-native/toolkit/editor` |
699
+ | Area | APIs | Import path |
700
+ | --------------- | ------------------------------------------------------------------------------ | -------------------------------------------------- |
701
+ | Agent page | `<AgentTabsPage>` | `@agent-native/toolkit/app/agent-page` |
702
+ | Blocks UI | `BlockView`, `SchemaBlockEditor`, `BlockRegistryProvider` | `@agent-native/toolkit/app/blocks` |
703
+ | Canvas | `DrawOverlay`, `CanvasCommentPins` | `@agent-native/toolkit/canvas-annotations` |
704
+ | Chat history | `ChatHistoryList`, `ChatHistoryRail` | `@agent-native/toolkit/chat-history` |
705
+ | Owned chat rail | `useOwnershipChatHistory()`, `OwnershipChatHistoryRail` | `@agent-native/toolkit/app/chat/ownership-history` |
706
+ | Data grid | `<DataGrid>` | `@agent-native/toolkit/data-grid` |
707
+ | Dashboard | `DataTable`, `DateRangePicker`, `GenericChartPanel`, `MetricCard`, `StatsCard` | `@agent-native/toolkit/dashboard` |
708
+ | Sharing | `<ShareButton>`, `<ShareDialog>` | `@agent-native/toolkit/app/sharing` |
709
+ | Sharing UI | `<VisibilityBadge>` | `@agent-native/toolkit/sharing` |
710
+ | Notifications | `<NotificationsBell>` | `@agent-native/toolkit/app/notifications` |
711
+ | Progress | `<RunsTray>` | `@agent-native/toolkit/app/progress` |
712
+ | Run registry | Run types and server registry functions | `@agent-native/core/progress` |
713
+ | Onboarding | `useOnboarding()`, onboarding panel hooks | `@agent-native/core/client/onboarding` |
714
+ | Observability | `<ObservabilityDashboard>`, `<ThumbsFeedback>` | `@agent-native/toolkit/app/observability` |
715
+ | Resources UI | `<ResourcesPanel>`, `<ResourceTree>`, `<ResourceEditor>` | `@agent-native/toolkit/app/resources` |
716
+ | Resource hooks | Resource data hooks | `@agent-native/core/client/resources` |
717
+ | Rich editor | `<SharedRichEditor>`, slash commands, block node hooks | `@agent-native/toolkit/editor` |
717
718
 
718
719
  ## One-Off Text Completion {#one-off-text-completion}
719
720
 
@@ -194,6 +194,10 @@ const shared = useChatThreadList({ ownership: "shared", pageSize: 50 });
194
194
 
195
195
  A failed request sets `error` and keeps the rows already loaded. It never looks like an empty list. `<ShareButton>` and `<ShareDialog>` refresh mounted chat lists after they save. If you change a chat's sharing some other way, call `notifyChatThreadsUpdated()` so mounted lists refetch.
196
196
 
197
+ For a ready-made sidebar that combines owned chats, shared chats, and eligible started drafts, import `useOwnershipChatHistory()` and `OwnershipChatHistoryRail` from `@agent-native/toolkit/app/chat/ownership-history`. The adapter keeps owner and access-basis details on shared rows, pages each section independently, and retains rows and local optimistic changes when a refresh fails. Its mutation callbacks return `Promise<boolean>`, allowing the app to navigate only after a write succeeds. See [Sharing Kit](/docs/toolkit-sharing#shared-chat-history) for an example.
198
+
199
+ An active empty draft remains selectable and runnable while its first save is pending. Until persistence is confirmed, hide pin, rename, and archive actions, and the adapter rejects calls to those callbacks. `ownDraftRows` supplies persistence provenance for owned drafts, while confirmed owned rows from the server list keep their normal actions. For custom `OwnDraftRow` values, set optional `persisted?: boolean` to `true` only for a confirmed persisted server row owned by the current user. Do not infer persistence from message count or active ID. Role, privacy, and whether the active draft is empty remain separate facts.
200
+
197
201
  ## Same model, agent and UI {#agent-and-ui}
198
202
 
199
203
  The framework auto-mounts these actions in every template — the agent calls them as tools, and the UI calls them through `useActionQuery` / `useActionMutation`:
@@ -138,66 +138,46 @@ tokens that back agent-readable public surfaces.
138
138
 
139
139
  ## Shared chat history {#shared-chat-history}
140
140
 
141
- `@agent-native/toolkit/chat-history` renders the chat sidebar. Core supplies the
142
- rows and Toolkit renders them, so an app can show My chats and Shared with you
143
- as separate sections. See [Sharing & Privacy](/docs/sharing#shared-chats) for
144
- the `ownership` filter and the `ownerEmail` and `accessBasis` fields.
145
-
146
- `<ChatHistoryRail>` accepts `sections` as well as a flat `items` list. Each
147
- section previews five rows and has its own **Show all** and **Show less**
148
- control, loading, error, and empty states, and optional load-more:
141
+ `@agent-native/toolkit/chat-history` provides the presentational rail. For an app sidebar backed by Core chat ownership and lifecycle behavior, use the focused adapter at `@agent-native/toolkit/app/chat/ownership-history`:
149
142
 
150
143
  ```tsx
151
- import { ChatHistoryRail } from "@agent-native/toolkit/chat-history";
152
- import { useChatThreadList } from "@agent-native/core/client";
153
-
154
- const mine = useChatThreadList({ ownership: "mine" });
155
- const shared = useChatThreadList({ ownership: "shared" });
144
+ import {
145
+ ownDraftRows,
146
+ OwnershipChatHistoryRail,
147
+ useOwnershipChatHistory,
148
+ } from "@agent-native/toolkit/app/chat/ownership-history";
149
+ import { useChatThreads } from "@agent-native/core/client/agent-chat";
150
+
151
+ const { threads, isNewThread, pinThread, renameThread, archiveThread } =
152
+ useChatThreads(undefined, CHAT_STORAGE_KEY, null, { autoCreate: false });
153
+
154
+ const history = useOwnershipChatHistory({
155
+ apiUrl,
156
+ scope: { type: "workspace", id: workspaceId },
157
+ activeId: activeThreadId,
158
+ pinned: true,
159
+ localMineRows: ownDraftRows({ threads, isNewThread }),
160
+ mutations: {
161
+ pin: pinThread,
162
+ rename: renameThread,
163
+ archive: archiveThread,
164
+ },
165
+ });
156
166
 
157
- <ChatHistoryRail
167
+ <OwnershipChatHistoryRail
168
+ {...history}
169
+ activeId={activeThreadId}
158
170
  onNewChat={startChat}
159
- railLabels={{
160
- newChat: t("newChat"),
161
- showMore: t("showMore"),
162
- showLess: t("showLess"),
163
- sections: {
164
- showAll: t("showAll"),
165
- showLess: t("showLess"),
166
- loadMore: t("loadMore"),
167
- loadingMore: t("loadingMore"),
168
- retry: t("retry"),
169
- },
170
- }}
171
- sections={[
172
- { id: "mine", label: t("myChats"), items: toItems(mine.threads) },
173
- {
174
- id: "shared",
175
- label: t("sharedWithYou"),
176
- items: toItems(shared.threads),
177
- hasMore: shared.hasMore,
178
- loadingMore: shared.loadingMore,
179
- onLoadMore: shared.loadMore,
180
- loading: shared.loading,
181
- error: shared.error ? t("loadFailed") : undefined,
182
- onRetry: shared.refresh,
183
- },
184
- ]}
171
+ onSelect={selectThread}
172
+ showNewChat={false}
185
173
  />;
186
174
  ```
187
175
 
188
- With `sections`, `railLabels.sections` is required and has no defaults. Pass
189
- `showAll`, `showLess`, `loadMore`, `loadingMore`, and `retry` as translated text,
190
- and override them per section with `section.labels` when needed.
176
+ `useOwnershipChatHistory()` requests owned and shared rows from Core before paging. It keeps owner and access basis on shared rows. When pinned grouping is enabled with `pinned: true`, it separates pinned chats from the viewer's other owned chats. Started drafts are not inferred automatically. If the app has local lifecycle data, pass `localMineRows: ownDraftRows({ threads, isNewThread })` to include started drafts proven to belong to the current user. The rail previews five rows in each section, supports Show all and independent paging, and keeps loaded rows visible when a section request fails. Retry preserves local changes until a successful authoritative refresh can reconcile them.
191
177
 
192
- Show all reveals the rows already loaded. When the server has more, a disclosed
193
- section also offers Load more. A failed load shows above the rows that already
194
- loaded, so it never reads as an empty section.
178
+ The adapter supplies localized section labels. `railLabels` may override `newChat`, `showMore`, or `showLess` individually. `onNewChat` and `onSelect` are required. Pass `activeId` to both the hook and rail. Mutation callbacks return `Promise<boolean>` so failed writes can roll back without navigating away from the active chat.
195
179
 
196
- Rows can show who owns a chat and why the viewer can see it. Set `ownerLabel`,
197
- `accessLabel`, and `accessBasis` on a `ChatHistoryItem`. The app passes
198
- translated text, because the Toolkit list ships none. Shared rows should show a
199
- direct grant over organization access when both apply, which is how Core
200
- reports `accessBasis`.
180
+ For custom layouts, use `chatThreadToHistoryItem()` to convert a Core row and `ownDraftRows()` to derive eligible started drafts. `ChatHistoryRail` remains available when an app already owns its data and lifecycle logic. See [Sharing & Privacy](/docs/sharing#shared-chats) for ownership filters and access fields.
201
181
 
202
182
  ## What's next
203
183
 
@@ -3330,7 +3330,7 @@
3330
3330
  "to": "@agent-native/core/client/sharing",
3331
3331
  "symbols": {
3332
3332
  "VisibilityBadge": {
3333
- "to": "@agent-native/toolkit/sharing"
3333
+ "to": "@agent-native/toolkit/app/sharing"
3334
3334
  },
3335
3335
  "ShareButton": {
3336
3336
  "to": "@agent-native/toolkit/app/sharing"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.217.0",
3
+ "version": "0.218.0",
4
4
  "description": "The agentic application framework for building autonomous agents with intuitive UIs",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -730,8 +730,8 @@
730
730
  "yaml": "^2.9.0",
731
731
  "yjs": "^13.6.33",
732
732
  "zod": "^4.3.6",
733
- "@agent-native/agentkit": "^0.217.0",
734
- "@agent-native/recap-cli": "^0.5.86"
733
+ "@agent-native/recap-cli": "^0.5.87",
734
+ "@agent-native/agentkit": "^0.218.0"
735
735
  },
736
736
  "devDependencies": {
737
737
  "@ai-sdk/anthropic": "^3.0.71",
@@ -9,6 +9,7 @@
9
9
  "app/components/layout/AgentInspector.test.tsx",
10
10
  "app/components/layout/AgentInspector.tsx",
11
11
  "app/components/layout/Header.tsx",
12
+ "app/components/layout/Sidebar.spec.tsx",
12
13
  "app/components/layout/Sidebar.tsx",
13
14
  "app/i18n",
14
15
  "app/i18n-data.ts",
@@ -1,3 +1,4 @@
1
+ import { appPath } from "@agent-native/core/client/api-path";
1
2
  import { useT } from "@agent-native/core/client/i18n";
2
3
  import {
3
4
  navigateWithAgentChatViewTransition,
@@ -5,14 +6,15 @@ import {
5
6
  useChatThreads,
6
7
  type ChatThreadSummary,
7
8
  } from "@agent-native/toolkit/app/chat/agentkit-chat/rail";
9
+ import {
10
+ ownDraftRows,
11
+ OwnershipChatHistoryRail,
12
+ useOwnershipChatHistory,
13
+ type OwnDraftRow,
14
+ } from "@agent-native/toolkit/app/chat/ownership-history";
8
15
  import { OrgSwitcher } from "@agent-native/toolkit/app/org";
9
16
  import { openCommandMenu } from "@agent-native/toolkit/app/shared";
10
17
  import { AgentNativeIcon } from "@agent-native/toolkit/app/shared/AgentNativeIcon";
11
- import {
12
- ChatHistoryList,
13
- type ChatHistoryItem,
14
- type ChatHistorySection,
15
- } from "@agent-native/toolkit/chat-history/ChatHistoryList";
16
18
  import {
17
19
  IconApps,
18
20
  IconClock,
@@ -25,7 +27,7 @@ import {
25
27
  IconPin,
26
28
  IconSearch,
27
29
  } from "@tabler/icons-react";
28
- import { useEffect, useMemo, useRef, useState } from "react";
30
+ import { useEffect, useRef, useState } from "react";
29
31
  import { Link, useLocation, useNavigate } from "react-router";
30
32
  import { toast } from "sonner";
31
33
 
@@ -41,7 +43,6 @@ import {
41
43
  } from "@/components/ui/tooltip";
42
44
  import { APP_TITLE } from "@/lib/app-config";
43
45
  import { chatThreadPath, HOME_PATH, threadIdFromPath } from "@/lib/chat-paths";
44
- import { visibleChatThreads } from "@/lib/sidebar-thread-state";
45
46
  import { cn } from "@/lib/utils";
46
47
 
47
48
  const CHAT_STORAGE_KEY = "chat";
@@ -53,11 +54,9 @@ interface SidebarProps {
53
54
  onCollapsedChange?: (collapsed: boolean) => void;
54
55
  }
55
56
 
56
- function threadTitle(thread: ChatThreadSummary, untitledLabel: string) {
57
- return thread.title || untitledLabel;
58
- }
59
-
60
- function threadUpdatedAt(thread: ChatThreadSummary) {
57
+ function threadUpdatedAt(
58
+ thread: Pick<ChatThreadSummary, "createdAt" | "updatedAt">,
59
+ ) {
61
60
  return Number.isFinite(thread.updatedAt)
62
61
  ? thread.updatedAt
63
62
  : Number.isFinite(thread.createdAt)
@@ -65,14 +64,6 @@ function threadUpdatedAt(thread: ChatThreadSummary) {
65
64
  : 0;
66
65
  }
67
66
 
68
- function compareRecentThreads(a: ChatThreadSummary, b: ChatThreadSummary) {
69
- return threadUpdatedAt(b) - threadUpdatedAt(a);
70
- }
71
-
72
- function comparePinnedThreads(a: ChatThreadSummary, b: ChatThreadSummary) {
73
- return (b.pinnedAt ?? 0) - (a.pinnedAt ?? 0);
74
- }
75
-
76
67
  function formatRelativeTime(timestamp: number) {
77
68
  const elapsed = timestamp - Date.now();
78
69
  const intervals = [
@@ -106,7 +97,10 @@ function ThreadHistoryTitle({
106
97
  title,
107
98
  running,
108
99
  }: {
109
- thread: ChatThreadSummary;
100
+ thread: OwnDraftRow &
101
+ Partial<
102
+ Pick<ChatThreadSummary, "preview" | "scope" | "source" | "pinnedAt">
103
+ >;
110
104
  title: string;
111
105
  running: boolean;
112
106
  }) {
@@ -287,7 +281,7 @@ function ChatThreadsSection({ collapsed }: { collapsed: boolean }) {
287
281
  pinThread,
288
282
  archiveThread,
289
283
  renameThread,
290
- refreshThreads,
284
+ isNewThread,
291
285
  } = useChatThreads(undefined, CHAT_STORAGE_KEY, undefined, {
292
286
  autoCreate: false,
293
287
  restoreActiveThread: false,
@@ -295,66 +289,46 @@ function ChatThreadsSection({ collapsed }: { collapsed: boolean }) {
295
289
  const { workingThreadIds, observedThreadStarts } =
296
290
  useAgentChatRunningThreads();
297
291
 
298
- const visibleThreads = useMemo(
299
- () => visibleChatThreads(threads, observedThreadStarts),
300
- [observedThreadStarts, threads],
301
- );
302
- const displayedActiveThreadId =
303
- threadIdFromPath(location.pathname) ??
304
- activeThreadId ??
305
- persistedActiveThreadId();
306
-
307
- const historySections = useMemo<ChatHistorySection[]>(() => {
308
- const untitledLabel = t("chat.untitledChat");
309
- const toItem = (thread: ChatThreadSummary): ChatHistoryItem => {
310
- const title = threadTitle(thread, untitledLabel);
292
+ const localMineRows = ownDraftRows({
293
+ threads,
294
+ isNewThread,
295
+ observedThreadStarts,
296
+ });
297
+ const history = useOwnershipChatHistory({
298
+ getThreadUrl: (threadId) => appPath(chatThreadPath(threadId)),
299
+ activeId:
300
+ threadIdFromPath(location.pathname) ??
301
+ activeThreadId ??
302
+ persistedActiveThreadId(),
303
+ localMineRows,
304
+ pinned: { label: t("chat.pinned") },
305
+ mutations: {
306
+ pin: pinThread,
307
+ rename: renameThread,
308
+ archive: archiveThread,
309
+ },
310
+ onMutationFailed: (kind) => {
311
+ if (kind === "archive") toast.error(t("chat.archiveFailed"));
312
+ if (kind === "rename") toast.error(t("chat.renameFailed"));
313
+ },
314
+ decorateItem: (item, row) => {
315
+ const title = row.title || row.preview || t("chat.untitledChat");
311
316
  return {
312
- id: thread.id,
317
+ ...item,
313
318
  title: (
314
319
  <ThreadHistoryTitle
315
- thread={thread}
320
+ thread={row}
316
321
  title={title}
317
- running={workingThreadIds.has(thread.id)}
322
+ running={workingThreadIds.has(row.id)}
318
323
  />
319
324
  ),
320
- titleText: title,
321
- pinned: Boolean(thread.pinnedAt),
322
325
  };
323
- };
324
- const pinned = visibleThreads
325
- .filter((thread) => Boolean(thread.pinnedAt))
326
- .sort(comparePinnedThreads)
327
- .map(toItem);
328
- const recent = visibleThreads
329
- .filter((thread) => !thread.pinnedAt)
330
- .sort(compareRecentThreads)
331
- .slice(0, 30)
332
- .map(toItem);
333
-
334
- return [
335
- { id: "pinned", label: t("chat.pinned"), items: pinned },
336
- { id: "recents", label: t("chat.recents"), items: recent },
337
- ];
338
- }, [t, visibleThreads, workingThreadIds]);
339
-
340
- useEffect(() => {
341
- const refresh = () => refreshThreads();
342
- const handleRunning = (event: Event) => {
343
- const detail = (event as CustomEvent).detail as
344
- | { isRunning?: unknown }
345
- | undefined;
346
- if (typeof detail?.isRunning === "boolean") refreshThreads();
347
- };
348
-
349
- window.addEventListener("agent-chat:threads-updated", refresh);
350
- window.addEventListener("agentNative.chatRunning", handleRunning);
351
- window.addEventListener("focus", refresh);
352
- return () => {
353
- window.removeEventListener("agent-chat:threads-updated", refresh);
354
- window.removeEventListener("agentNative.chatRunning", handleRunning);
355
- window.removeEventListener("focus", refresh);
356
- };
357
- }, [refreshThreads]);
326
+ },
327
+ });
328
+ const displayedActiveThreadId =
329
+ threadIdFromPath(location.pathname) ??
330
+ activeThreadId ??
331
+ persistedActiveThreadId();
358
332
 
359
333
  function openThread(threadId: string, options?: { isNew?: boolean }) {
360
334
  switchThread(threadId);
@@ -382,23 +356,6 @@ function ChatThreadsSection({ collapsed }: { collapsed: boolean }) {
382
356
  if (threadId) openThread(threadId, { isNew: true });
383
357
  }
384
358
 
385
- async function handleArchiveThread(threadId: string) {
386
- const wasActive =
387
- threadId === activeThreadId || threadId === persistedActiveThreadId();
388
- const archived = await archiveThread(threadId);
389
- if (!archived) {
390
- toast.error(t("chat.archiveFailed"));
391
- return;
392
- }
393
- if (wasActive) await handleNewChat();
394
- }
395
-
396
- function handleRenameThread(threadId: string, title: string) {
397
- void renameThread(threadId, title).then((renamed) => {
398
- if (!renamed) toast.error(t("chat.renameFailed"));
399
- });
400
- }
401
-
402
359
  const newChatButton = (
403
360
  <button
404
361
  type="button"
@@ -430,17 +387,20 @@ function ChatThreadsSection({ collapsed }: { collapsed: boolean }) {
430
387
  return (
431
388
  <div className="flex min-h-0 flex-1 flex-col">
432
389
  <div className="px-2 pb-3">{newChatButton}</div>
433
- <ChatHistoryList
434
- sections={historySections}
390
+ <OwnershipChatHistoryRail
391
+ {...history}
392
+ onNewChat={() => void handleNewChat()}
393
+ showNewChat={false}
435
394
  activeId={displayedActiveThreadId}
436
395
  onSelect={openThread}
437
- onTogglePin={(threadId) => {
438
- const thread = visibleThreads.find((item) => item.id === threadId);
439
- if (thread) void pinThread(threadId, !thread.pinnedAt);
396
+ onDelete={async (threadId) => {
397
+ const wasActive =
398
+ threadId === activeThreadId ||
399
+ threadId === persistedActiveThreadId();
400
+ const archived = await history.listProps.onDelete?.(threadId);
401
+ if (archived && wasActive) await handleNewChat();
440
402
  }}
441
- onRename={handleRenameThread}
442
403
  renameMaxLength={160}
443
- onDelete={(threadId) => void handleArchiveThread(threadId)}
444
404
  labels={{
445
405
  options: (item) =>
446
406
  t("chat.optionsFor", { title: item.titleText ?? "" }),
@@ -451,8 +411,9 @@ function ChatThreadsSection({ collapsed }: { collapsed: boolean }) {
451
411
  unpin: t("chat.unpinChat"),
452
412
  delete: t("chat.archiveChat"),
453
413
  }}
454
- variant="rail"
455
- emptyLabel={null}
414
+ railLabels={{
415
+ newChat: t("chat.newChat"),
416
+ }}
456
417
  className="chat-sidebar-history min-h-0 flex-1"
457
418
  listClassName="pb-4"
458
419
  />
@@ -1,29 +0,0 @@
1
- import type { ChatThreadSummary } from "@agent-native/toolkit/app/chat/agentkit-chat/index";
2
-
3
- export function visibleChatThreads(
4
- threads: ChatThreadSummary[],
5
- observedThreadStarts: ReadonlyMap<string, number>,
6
- ): ChatThreadSummary[] {
7
- const knownIds = new Set(threads.map((thread) => thread.id));
8
- const optimistic: ChatThreadSummary[] = [...observedThreadStarts].flatMap(
9
- ([id, startedAt]) =>
10
- knownIds.has(id)
11
- ? []
12
- : [
13
- {
14
- id,
15
- title: "",
16
- preview: "",
17
- messageCount: 0,
18
- createdAt: startedAt,
19
- updatedAt: startedAt,
20
- scope: null,
21
- },
22
- ],
23
- );
24
- return [...threads, ...optimistic].filter(
25
- (thread) =>
26
- !thread.archivedAt &&
27
- (thread.messageCount > 0 || observedThreadStarts.has(thread.id)),
28
- );
29
- }
@@ -1,29 +0,0 @@
1
- import type { ChatThreadSummary } from "@agent-native/toolkit/app/chat/agentkit-chat/index";
2
-
3
- export function visibleChatThreads(
4
- threads: ChatThreadSummary[],
5
- observedThreadStarts: ReadonlyMap<string, number>,
6
- ): ChatThreadSummary[] {
7
- const knownIds = new Set(threads.map((thread) => thread.id));
8
- const optimistic: ChatThreadSummary[] = [...observedThreadStarts].flatMap(
9
- ([id, startedAt]) =>
10
- knownIds.has(id)
11
- ? []
12
- : [
13
- {
14
- id,
15
- title: "",
16
- preview: "",
17
- messageCount: 0,
18
- createdAt: startedAt,
19
- updatedAt: startedAt,
20
- scope: null,
21
- },
22
- ],
23
- );
24
- return [...threads, ...optimistic].filter(
25
- (thread) =>
26
- !thread.archivedAt &&
27
- (thread.messageCount > 0 || observedThreadStarts.has(thread.id)),
28
- );
29
- }