@agent-native/dispatch 0.15.2 → 0.15.3

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 (43) hide show
  1. package/dist/actions/delete-staged-dataset.d.ts +3 -1
  2. package/dist/actions/delete-staged-dataset.js +3 -26
  3. package/dist/actions/delete-staged-dataset.js.map +1 -1
  4. package/dist/actions/delete-workspace-resource.d.ts +13 -13
  5. package/dist/actions/list-staged-datasets.d.ts +1 -1
  6. package/dist/actions/list-staged-datasets.js +3 -28
  7. package/dist/actions/list-staged-datasets.js.map +1 -1
  8. package/dist/actions/provider-api-catalog.d.ts +6 -4
  9. package/dist/actions/provider-api-catalog.js +3 -17
  10. package/dist/actions/provider-api-catalog.js.map +1 -1
  11. package/dist/actions/provider-api-docs.d.ts +21 -2
  12. package/dist/actions/provider-api-docs.js +6 -51
  13. package/dist/actions/provider-api-docs.js.map +1 -1
  14. package/dist/actions/provider-api-register.d.ts +39 -15
  15. package/dist/actions/provider-api-register.js +4 -193
  16. package/dist/actions/provider-api-register.js.map +1 -1
  17. package/dist/actions/provider-api-request.d.ts +34 -2
  18. package/dist/actions/provider-api-request.js +6 -171
  19. package/dist/actions/provider-api-request.js.map +1 -1
  20. package/dist/actions/query-staged-dataset.d.ts +18 -1
  21. package/dist/actions/query-staged-dataset.d.ts.map +1 -1
  22. package/dist/actions/query-staged-dataset.js +3 -100
  23. package/dist/actions/query-staged-dataset.js.map +1 -1
  24. package/dist/actions/upsert-destination.d.ts +12 -12
  25. package/dist/components/layout/Layout.d.ts.map +1 -1
  26. package/dist/components/layout/Layout.js +26 -64
  27. package/dist/components/layout/Layout.js.map +1 -1
  28. package/package.json +4 -3
  29. package/src/actions/delete-staged-dataset.ts +3 -33
  30. package/src/actions/list-staged-datasets.ts +3 -30
  31. package/src/actions/provider-api-audit.spec.ts +2 -3
  32. package/src/actions/provider-api-catalog.ts +10 -23
  33. package/src/actions/provider-api-docs.ts +19 -68
  34. package/src/actions/provider-api-register.ts +5 -233
  35. package/src/actions/provider-api-request.ts +19 -232
  36. package/src/actions/query-staged-dataset.ts +3 -119
  37. package/src/components/layout/Layout.spec.tsx +7 -11
  38. package/src/components/layout/Layout.tsx +42 -145
  39. package/dist/actions/provider-api-audit.d.ts +0 -7
  40. package/dist/actions/provider-api-audit.d.ts.map +0 -1
  41. package/dist/actions/provider-api-audit.js +0 -74
  42. package/dist/actions/provider-api-audit.js.map +0 -1
  43. package/src/actions/provider-api-audit.ts +0 -88
@@ -1,241 +1,28 @@
1
- import { defineAction } from "@agent-native/core";
2
- import { stagingExecuteRequest } from "@agent-native/core/provider-api/staging";
3
- import { getCredentialContext } from "@agent-native/core/server/request-context";
1
+ import {
2
+ createProviderApiRequestAction,
3
+ createProviderApiRequestSchema,
4
+ } from "@agent-native/core/provider-api/actions/provider-api";
4
5
  import { z } from "zod";
5
6
 
6
7
  import {
7
8
  DISPATCH_APP_ID,
8
9
  executeProviderApiRequest,
9
10
  } from "../server/lib/provider-api.js";
10
- import { buildProviderApiAuditSummary } from "./provider-api-audit.js";
11
-
12
- const MethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
13
11
 
14
- const PaginationSchema = z
15
- .object({
16
- nextCursorPath: z
17
- .string()
18
- .optional()
19
- .describe(
20
- "Dot-path in the response JSON where the next cursor/token lives, e.g. 'next_cursor', 'meta.next', or 'nextPageToken'.",
21
- ),
22
- cursorParam: z
23
- .string()
24
- .optional()
25
- .describe(
26
- "Query parameter name to inject the cursor into the next request. Use cursorBodyPath for APIs that page through POST bodies.",
27
- ),
28
- cursorBodyPath: z
29
- .string()
30
- .optional()
31
- .describe(
32
- "Dot-path in the JSON request body to set to the next cursor. Use this for POST-body pagination.",
33
- ),
34
- pageParam: z
35
- .string()
36
- .optional()
37
- .describe(
38
- "Use page-number mode: this query param is incremented on each page.",
39
- ),
40
- startPage: z.coerce
41
- .number()
42
- .int()
43
- .optional()
44
- .describe("Starting page number for pageParam mode (default 1)."),
45
- offsetParam: z
46
- .string()
47
- .optional()
48
- .describe(
49
- "Use offset mode: this query param is incremented by pageSize on each request.",
50
- ),
51
- pageSize: z.coerce
52
- .number()
53
- .int()
54
- .min(1)
55
- .optional()
56
- .describe(
57
- "Expected page size for offset increments. Defaults to the actual item count of the first page.",
58
- ),
59
- maxPages: z.coerce
60
- .number()
61
- .int()
62
- .min(1)
63
- .max(200)
64
- .optional()
65
- .describe("Maximum pages to fetch server-side (default 50, max 200)."),
66
- })
67
- .optional();
68
-
69
- export default defineAction({
70
- description:
71
- "Make an arbitrary authenticated HTTP request to a shared workspace integration, configured provider API, or custom provider registered via provider-api-register. Use this as the flexible escape hatch when Dispatch needs a provider endpoint, filter, pagination mode, payload, or API version that no canned action models. The request is constrained to the provider host, uses configured credentials automatically, blocks private/internal URLs, and redacts secrets from responses.",
72
- schema: z.object({
73
- provider: z
74
- .string()
75
- .min(1)
76
- .describe(
77
- "Provider id to call — built-in (e.g. slack, github, notion, hubspot, gmail, google_drive, google_calendar, granola, stripe, jira) or a custom provider id registered via provider-api-register. Use provider-api-catalog to list available providers.",
78
- ),
79
- method: MethodSchema.default("GET").describe("HTTP method to use."),
80
- path: z
81
- .string()
82
- .min(1)
83
- .describe(
84
- "Provider API path such as /search.messages, /repos/org/repo/issues, /crm/v3/objects/deals/search, or a full URL on an allowed provider host. Use placeholders from provider-api-catalog when provided.",
85
- ),
86
- query: z
87
- .unknown()
88
- .optional()
89
- .describe(
90
- "Optional query params as a JSON object/string. Array values produce repeated query params.",
91
- ),
92
- headers: z
93
- .record(z.string(), z.unknown())
94
- .optional()
95
- .describe(
96
- "Optional extra headers. Unsafe hop-by-hop headers are ignored. Auth headers are injected from stored credentials.",
97
- ),
98
- body: z
99
- .unknown()
100
- .optional()
101
- .describe(
102
- "Optional request body. Objects/arrays are JSON encoded; strings are sent as-is.",
103
- ),
104
- auth: z
105
- .enum(["default", "none"])
106
- .default("default")
107
- .describe(
108
- "Use default to inject configured provider auth. Use none only for public provider endpoints that intentionally require no auth.",
109
- ),
110
- connectionId: z
111
- .string()
112
- .optional()
113
- .describe(
114
- "Optional shared workspace connection id to use when the provider has multiple granted connections.",
115
- ),
116
- accountId: z
117
- .string()
118
- .optional()
119
- .describe(
120
- "Optional OAuth account id to use for OAuth-backed providers such as Gmail, Google Calendar, or Google Drive.",
121
- ),
122
- timeoutMs: z.coerce
123
- .number()
124
- .int()
125
- .min(1_000)
126
- .max(120_000)
127
- .optional()
128
- .describe("Request timeout in milliseconds. Default 30000, max 120000."),
129
- maxBytes: z.coerce
130
- .number()
131
- .int()
132
- .min(1_000)
133
- .max(4 * 1024 * 1024)
134
- .optional()
135
- .describe(
136
- "Maximum response bytes to read. Default 1MB, max 4MB. Ignored when saveToFile is set (allows up to 20MB).",
137
- ),
138
- saveToFile: z
139
- .string()
140
- .optional()
141
- .describe(
142
- "Workspace file path to save the full response body to instead of returning it in context (e.g. 'analysis/provider-response.json'). When set, returns only a compact summary {savedTo, status, bytes, preview} and allows up to 20MB response. Useful for large datasets that would overflow context.",
143
- ),
144
- stageAs: z
145
- .string()
146
- .min(1)
147
- .optional()
148
- .describe(
149
- "When set, parse the response as an array of records and write them into a staged dataset with this name. Returns a compact summary instead of the raw body. Re-staging the same name replaces the previous dataset.",
150
- ),
151
- itemsPath: z
152
- .string()
153
- .optional()
154
- .describe(
155
- "Dot-path to the items array in the response JSON, e.g. 'items', 'results', or 'data'. Omit for auto-detection.",
156
- ),
157
- pagination: PaginationSchema.describe(
158
- "Pagination config for server-side fetchAll when stageAs is set. Supports cursor (nextCursorPath + cursorParam or cursorBodyPath), page, and offset modes.",
12
+ export default createProviderApiRequestAction(
13
+ { executeRequest: executeProviderApiRequest },
14
+ {
15
+ appId: DISPATCH_APP_ID,
16
+ description:
17
+ "Make an arbitrary authenticated HTTP request to a shared workspace integration, configured provider API, or custom provider registered via provider-api-register. Use this as the flexible escape hatch when Dispatch needs a provider endpoint, filter, pagination mode, payload, or API version that no canned action models. The request is constrained to the provider host, uses configured credentials automatically, blocks private/internal URLs, and redacts secrets from responses.",
18
+ schema: createProviderApiRequestSchema(
19
+ z
20
+ .string()
21
+ .min(1)
22
+ .describe(
23
+ "Provider id to call — built-in (e.g. slack, github, notion, hubspot, gmail, google_drive, google_calendar, granola, stripe, jira) or a custom provider id registered via provider-api-register. Use provider-api-catalog to list available providers.",
24
+ ),
159
25
  ),
160
- fetchAllPages: z
161
- .object({
162
- cursorPath: z
163
- .string()
164
- .describe(
165
- "Dot-path in the JSON response body where the next-page cursor lives, e.g. 'meta.next_cursor' or 'pagination.next_page_token'.",
166
- ),
167
- cursorParam: z
168
- .string()
169
- .optional()
170
- .describe(
171
- "Query parameter name to pass the cursor on subsequent pages, e.g. 'cursor' or 'page_token'. Use cursorBodyPath instead for APIs that put cursors in POST bodies.",
172
- ),
173
- cursorBodyPath: z
174
- .string()
175
- .optional()
176
- .describe(
177
- "Dot-path in the JSON request body to set to the next cursor. Use for POST-body pagination.",
178
- ),
179
- itemsPath: z
180
- .string()
181
- .optional()
182
- .describe(
183
- "Dot-path to the items array in each response, e.g. 'results' or 'data.items'. When omitted, the whole response body is appended per page.",
184
- ),
185
- maxPages: z.coerce
186
- .number()
187
- .int()
188
- .min(1)
189
- .max(50)
190
- .optional()
191
- .describe(
192
- "Maximum pages to fetch. Default 10, max 50. Stops early when the cursor is empty.",
193
- ),
194
- })
195
- .optional()
196
- .describe(
197
- "Enable cursor-based pagination. After each response, reads cursorPath from the JSON body and re-issues the request with cursorParam or cursorBodyPath set, accumulating items from itemsPath (or whole bodies) until cursor is empty or maxPages is reached. Combine with saveToFile to write the full dataset to a workspace file.",
198
- ),
199
- }),
200
- http: false,
201
- audit: {
202
- recordInputs: false,
203
- target: (args) => ({
204
- type: "provider-api",
205
- id: String(args.provider),
206
- visibility: "private",
207
- }),
208
- summary: (args) => buildProviderApiAuditSummary(args),
209
- },
210
- run: async (args) => {
211
- if (args.stageAs) {
212
- const ctx = getCredentialContext();
213
- if (!ctx) {
214
- throw new Error("No authenticated context for provider API staging.");
215
- }
216
- return stagingExecuteRequest(
217
- {
218
- provider: args.provider,
219
- method: args.method,
220
- path: args.path,
221
- query: args.query,
222
- headers: args.headers,
223
- body: args.body,
224
- auth: args.auth,
225
- connectionId: args.connectionId,
226
- accountId: args.accountId,
227
- timeoutMs: args.timeoutMs,
228
- maxBytes: args.maxBytes,
229
- stageAs: args.stageAs,
230
- itemsPath: args.itemsPath,
231
- pagination: args.pagination,
232
- },
233
- (reqArgs) => executeProviderApiRequest(reqArgs),
234
- { appId: DISPATCH_APP_ID, ownerEmail: ctx.userEmail },
235
- );
236
- }
237
- return executeProviderApiRequest(
238
- args as unknown as Parameters<typeof executeProviderApiRequest>[0],
239
- );
26
+ http: false,
240
27
  },
241
- });
28
+ );
@@ -1,126 +1,10 @@
1
- import { defineAction } from "@agent-native/core";
2
- import { runAggregateQuery } from "@agent-native/core/provider-api/staged-datasets-aggregate";
3
- import {
4
- getStagedDatasetMeta,
5
- getStagedDatasetRows,
6
- } from "@agent-native/core/provider-api/staged-datasets-store";
7
- import { getCredentialContext } from "@agent-native/core/server/request-context";
8
- import { z } from "zod";
1
+ import { createQueryStagedDatasetAction } from "@agent-native/core/provider-api/actions/staged-datasets";
9
2
 
10
3
  import { DISPATCH_APP_ID } from "../server/lib/provider-api.js";
11
4
 
12
- const WhereSchema = z.object({
13
- column: z.string().min(1),
14
- op: z.enum([
15
- "equals",
16
- "not_equals",
17
- "contains",
18
- "not_contains",
19
- "gt",
20
- "gte",
21
- "lt",
22
- "lte",
23
- "exists",
24
- "not_exists",
25
- ]),
26
- value: z.unknown().optional(),
27
- });
28
-
29
- const AggregateFieldSchema = z.object({
30
- column: z.string().min(1).describe("Column to aggregate."),
31
- op: z
32
- .enum(["sum", "avg", "count", "min", "max", "count_distinct"])
33
- .describe("Aggregation function."),
34
- as: z
35
- .string()
36
- .optional()
37
- .describe("Output column name. Default: {op}_{column}."),
38
- });
39
-
40
- export default defineAction({
5
+ export default createQueryStagedDatasetAction({
6
+ appId: DISPATCH_APP_ID,
41
7
  description:
42
8
  "Run a filter/aggregate/project query over a staged dataset previously written by provider-api-request (stageAs). Use after staging provider records, messages, documents, issues, events, or search results to count, group, filter, or project rows without re-fetching provider APIs.",
43
- schema: z.object({
44
- datasetId: z
45
- .string()
46
- .min(1)
47
- .describe(
48
- "Dataset id from provider-api-request stageAs result, or from list-staged-datasets.",
49
- ),
50
- where: z
51
- .array(WhereSchema)
52
- .optional()
53
- .describe(
54
- "Row-level filters (AND). Ops: equals, not_equals, contains, not_contains, gt, gte, lt, lte, exists, not_exists.",
55
- ),
56
- groupBy: z
57
- .array(z.string().min(1))
58
- .optional()
59
- .describe(
60
- "Column(s) to group by. Omit for a single aggregate over all rows.",
61
- ),
62
- aggregate: z
63
- .array(AggregateFieldSchema)
64
- .optional()
65
- .describe(
66
- "Aggregation operations. When set, non-group columns are aggregated. Omit to return raw rows.",
67
- ),
68
- select: z
69
- .array(z.string().min(1))
70
- .optional()
71
- .describe("Column projection when aggregate is empty."),
72
- orderBy: z.string().optional().describe("Sort output by this column."),
73
- orderDir: z
74
- .enum(["asc", "desc"])
75
- .optional()
76
- .describe("Sort direction (default asc)."),
77
- limit: z.coerce
78
- .number()
79
- .int()
80
- .min(1)
81
- .max(10_000)
82
- .optional()
83
- .describe("Maximum rows to return (default all, max 10000)."),
84
- }),
85
9
  http: false,
86
- readOnly: true,
87
- run: async (args) => {
88
- const ctx = getCredentialContext();
89
- if (!ctx) {
90
- throw new Error("No authenticated context for query-staged-dataset.");
91
- }
92
-
93
- const meta = await getStagedDatasetMeta({
94
- id: args.datasetId,
95
- appId: DISPATCH_APP_ID,
96
- ownerEmail: ctx.userEmail,
97
- });
98
- if (!meta) {
99
- throw new Error(
100
- `Dataset ${args.datasetId} not found (or belongs to a different owner/app).`,
101
- );
102
- }
103
-
104
- const rows = await getStagedDatasetRows({
105
- id: args.datasetId,
106
- appId: DISPATCH_APP_ID,
107
- ownerEmail: ctx.userEmail,
108
- });
109
-
110
- const result = runAggregateQuery(rows, {
111
- where: args.where,
112
- groupBy: args.groupBy,
113
- aggregate: args.aggregate,
114
- select: args.select,
115
- orderBy: args.orderBy,
116
- orderDir: args.orderDir,
117
- limit: args.limit,
118
- });
119
-
120
- return {
121
- dataset: { id: meta.id, name: meta.name, totalRows: meta.rowCount },
122
- rowCount: result.length,
123
- rows: result,
124
- };
125
- },
126
10
  });
@@ -168,7 +168,7 @@ describe("Dispatch NavContent", () => {
168
168
  expect(lists[0].querySelector("a")?.className).toContain("h-8 w-8");
169
169
  });
170
170
 
171
- it("uses the quieter Analytics-style chat history and retains thread actions", async () => {
171
+ it("uses the shared chat history rail and retains thread actions", async () => {
172
172
  await act(async () => {
173
173
  root.render(
174
174
  <MemoryRouter initialEntries={["/chat/active-thread"]}>
@@ -184,18 +184,14 @@ describe("Dispatch NavContent", () => {
184
184
  expect(container.textContent).toContain("Earlier Dispatch work");
185
185
  expect(container.textContent).toContain("New chat");
186
186
  expect(container.textContent).toContain("5m");
187
- const age = [...container.querySelectorAll("time")].find(
187
+ const age = [...container.querySelectorAll("span")].find(
188
188
  (element) => element.textContent === "5m",
189
189
  );
190
- expect(age?.className).toContain("w-8");
191
- expect(age?.className).toContain("shrink-0");
192
- expect(age?.className).toContain("whitespace-nowrap");
193
- expect(age?.className).toContain("tabular-nums");
194
- expect(
195
- [...container.querySelectorAll("div")].some((element) =>
196
- element.className.includes("group/item"),
197
- ),
198
- ).toBe(true);
190
+ expect(age?.className).toContain("an-chat-history-row__timestamp");
191
+ const historyList = container.querySelector(
192
+ '[data-agent-native="chat-history-list"]',
193
+ );
194
+ expect(historyList?.className).toContain("an-chat-history--rail");
199
195
  expect(
200
196
  container.querySelector('img[src="/agent-native-icon-light.svg"]')
201
197
  ?.parentElement?.className,
@@ -13,6 +13,10 @@ import { useActionQuery } from "@agent-native/core/client/hooks";
13
13
  import { useT } from "@agent-native/core/client/i18n";
14
14
  import { InvitationBanner, OrgSwitcher } from "@agent-native/core/client/org";
15
15
  import { FeedbackButton } from "@agent-native/core/client/ui";
16
+ import {
17
+ ChatHistoryList,
18
+ type ChatHistoryItem,
19
+ } from "@agent-native/toolkit/chat-history";
16
20
  import {
17
21
  IconActivity,
18
22
  IconArrowUpRight,
@@ -22,8 +26,6 @@ import {
22
26
  IconBrandTelegram,
23
27
  IconKey,
24
28
  IconChevronDown,
25
- IconDots,
26
- IconEdit,
27
29
  IconLayersSubtract,
28
30
  IconMessageQuestion,
29
31
  IconMessages,
@@ -42,10 +44,8 @@ import {
42
44
  import {
43
45
  useEffect,
44
46
  useMemo,
45
- useRef,
46
47
  useState,
47
48
  type ComponentType,
48
- type FormEvent,
49
49
  type ReactNode,
50
50
  } from "react";
51
51
  import { NavLink, useLocation, useNavigate } from "react-router";
@@ -57,7 +57,6 @@ import {
57
57
  DropdownMenuItem,
58
58
  DropdownMenuTrigger,
59
59
  } from "../ui/dropdown-menu";
60
- import { Input } from "../ui/input";
61
60
  import { Sheet, SheetContent, SheetDescription, SheetTitle } from "../ui/sheet";
62
61
  import { Skeleton } from "../ui/skeleton";
63
62
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
@@ -348,10 +347,6 @@ function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
348
347
  renameThread,
349
348
  refreshThreads,
350
349
  } = useChatThreads(undefined, "dispatch", undefined, { autoCreate: false });
351
- const [renamingThreadId, setRenamingThreadId] = useState<string | null>(null);
352
- const [renameDraft, setRenameDraft] = useState("");
353
- const renameInputRef = useRef<HTMLInputElement | null>(null);
354
- const committingRenameRef = useRef(false);
355
350
 
356
351
  const visibleThreads = useMemo(
357
352
  () =>
@@ -363,6 +358,22 @@ function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
363
358
  .slice(0, 8),
364
359
  [activeThreadId, threads],
365
360
  );
361
+ const localPathname = localDispatchPath(location.pathname);
362
+ const displayedActiveThreadId =
363
+ threadIdFromPath(localPathname) ??
364
+ (localPathname === "/chat" ? null : activeThreadId);
365
+ const chatItems: ChatHistoryItem[] = visibleThreads.map((thread) => {
366
+ const title = threadTitle(thread, t("dispatch.sidebar.newChat"));
367
+ return {
368
+ id: thread.id,
369
+ title: <span title={title}>{title}</span>,
370
+ titleText: title,
371
+ timestamp:
372
+ thread.id === displayedActiveThreadId
373
+ ? ""
374
+ : formatThreadAge(threadUpdatedAt(thread)),
375
+ };
376
+ });
366
377
 
367
378
  useEffect(() => {
368
379
  const refresh = () => refreshThreads();
@@ -383,14 +394,6 @@ function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
383
394
  };
384
395
  }, [refreshThreads]);
385
396
 
386
- useEffect(() => {
387
- if (!renamingThreadId) return;
388
- requestAnimationFrame(() => {
389
- renameInputRef.current?.focus();
390
- renameInputRef.current?.select();
391
- });
392
- }, [renamingThreadId]);
393
-
394
397
  function openThread(threadId: string, options?: { isNew?: boolean }) {
395
398
  switchThread(threadId);
396
399
  navigateWithAgentChatViewTransition(
@@ -414,35 +417,6 @@ function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
414
417
  if (threadId) openThread(threadId, { isNew: true });
415
418
  }
416
419
 
417
- function startRenameThread(thread: ChatThreadSummary) {
418
- committingRenameRef.current = false;
419
- setRenameDraft(threadTitle(thread, t("dispatch.sidebar.newChat")));
420
- setRenamingThreadId(thread.id);
421
- }
422
-
423
- function cancelRenameThread() {
424
- committingRenameRef.current = true;
425
- setRenamingThreadId(null);
426
- setRenameDraft("");
427
- }
428
-
429
- async function commitRenameThread() {
430
- if (committingRenameRef.current) return;
431
- const threadId = renamingThreadId;
432
- const title = renameDraft.trim();
433
- if (!threadId) return;
434
- committingRenameRef.current = true;
435
- setRenamingThreadId(null);
436
- setRenameDraft("");
437
- if (title) await renameThread(threadId, title);
438
- committingRenameRef.current = false;
439
- }
440
-
441
- function handleRenameSubmit(event: FormEvent<HTMLFormElement>) {
442
- event.preventDefault();
443
- void commitRenameThread();
444
- }
445
-
446
420
  return (
447
421
  <div className="ms-4 min-w-0 space-y-0.5">
448
422
  {chatsLoading &&
@@ -456,105 +430,28 @@ function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
456
430
  <Skeleton className="h-3 w-3/4 rounded" />
457
431
  </div>
458
432
  ))}
459
- {visibleThreads.map((thread) => {
460
- const localPathname = localDispatchPath(location.pathname);
461
- const isActive =
462
- thread.id ===
463
- (threadIdFromPath(localPathname) ??
464
- (localPathname === "/chat" ? null : activeThreadId));
465
- const isRenaming = thread.id === renamingThreadId;
466
- const title = threadTitle(thread, t("dispatch.sidebar.newChat"));
467
- return (
468
- <div
469
- key={thread.id}
470
- className={cn(
471
- "group/item relative flex min-w-0 items-center rounded-lg transition-colors",
472
- isActive
473
- ? "bg-sidebar-accent text-sidebar-accent-foreground"
474
- : "text-muted-foreground hover:bg-sidebar-accent/50 hover:text-foreground",
475
- )}
476
- >
477
- {isRenaming ? (
478
- <form
479
- onSubmit={handleRenameSubmit}
480
- className="flex min-w-0 flex-1 items-center px-1"
481
- >
482
- <Input
483
- ref={renameInputRef}
484
- value={renameDraft}
485
- onChange={(event) => setRenameDraft(event.target.value)}
486
- onBlur={() => void commitRenameThread()}
487
- onKeyDown={(event) => {
488
- if (event.key === "Escape") {
489
- event.preventDefault();
490
- cancelRenameThread();
491
- }
492
- }}
493
- maxLength={160}
494
- aria-label={t("dispatch.sidebar.renameThread", { title })}
495
- className="h-6 min-w-0 rounded-sm border-sidebar-border bg-background px-1.5 text-xs"
496
- />
497
- </form>
498
- ) : (
499
- <>
500
- <Tooltip>
501
- <TooltipTrigger asChild>
502
- <button
503
- type="button"
504
- onClick={() => openThread(thread.id)}
505
- className="flex min-w-0 flex-1 cursor-pointer items-center gap-2 px-2 py-1.5 pe-1 text-start text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring"
506
- >
507
- <span className="block min-w-0 flex-1 truncate">
508
- {title}
509
- </span>
510
- <time className="w-8 shrink-0 whitespace-nowrap text-end text-[11px] tabular-nums text-muted-foreground/60 transition-opacity group-hover/item:opacity-0 group-focus-within/item:opacity-0">
511
- {isActive
512
- ? ""
513
- : formatThreadAge(threadUpdatedAt(thread))}
514
- </time>
515
- </button>
516
- </TooltipTrigger>
517
- <TooltipContent side="right">{title}</TooltipContent>
518
- </Tooltip>
519
- <div className="pointer-events-none absolute end-1 top-1/2 flex -translate-y-1/2 items-center gap-0.5">
520
- <DropdownMenu>
521
- <Tooltip>
522
- <TooltipTrigger asChild>
523
- <DropdownMenuTrigger asChild>
524
- <button
525
- type="button"
526
- aria-label={t("dispatch.sidebar.chatOptions", {
527
- title,
528
- })}
529
- className="pointer-events-auto rounded p-0.5 text-muted-foreground/50 opacity-0 transition-[color,opacity] hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover/item:opacity-100 group-focus-within/item:opacity-100 data-[state=open]:opacity-100 data-[state=open]:text-foreground"
530
- >
531
- <IconDots className="size-3" />
532
- </button>
533
- </DropdownMenuTrigger>
534
- </TooltipTrigger>
535
- <TooltipContent side="right">
536
- {t("dispatch.sidebar.chatOptions", { title })}
537
- </TooltipContent>
538
- </Tooltip>
539
- <DropdownMenuContent
540
- align="start"
541
- side="right"
542
- className="w-44"
543
- >
544
- <DropdownMenuItem
545
- onSelect={() => startRenameThread(thread)}
546
- >
547
- <IconEdit className="size-3.5" />
548
- {t("dispatch.sidebar.renameChat")}
549
- </DropdownMenuItem>
550
- </DropdownMenuContent>
551
- </DropdownMenu>
552
- </div>
553
- </>
554
- )}
555
- </div>
556
- );
557
- })}
433
+ {visibleThreads.length > 0 && (
434
+ <ChatHistoryList
435
+ items={chatItems}
436
+ activeId={displayedActiveThreadId}
437
+ onSelect={(threadId) => openThread(threadId)}
438
+ renameMaxLength={160}
439
+ onRename={(threadId, title) => void renameThread(threadId, title)}
440
+ labels={{
441
+ options: (item) =>
442
+ t("dispatch.sidebar.chatOptions", {
443
+ title: item.titleText ?? "",
444
+ }),
445
+ renameInput: (item) =>
446
+ t("dispatch.sidebar.renameThread", {
447
+ title: item.titleText ?? "",
448
+ }),
449
+ rename: t("dispatch.sidebar.renameChat"),
450
+ }}
451
+ variant="rail"
452
+ className="min-w-0"
453
+ />
454
+ )}
558
455
  <button
559
456
  type="button"
560
457
  onClick={() => void handleNewChat()}
@@ -1,7 +0,0 @@
1
- export declare function sanitizeProviderApiAuditPath(path: unknown): string;
2
- export declare function buildProviderApiAuditSummary(args: {
3
- method?: unknown;
4
- provider?: unknown;
5
- path?: unknown;
6
- }): string;
7
- //# sourceMappingURL=provider-api-audit.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"provider-api-audit.d.ts","sourceRoot":"","sources":["../../src/actions/provider-api-audit.ts"],"names":[],"mappings":"AA8DA,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAclE;AAED,wBAAgB,4BAA4B,CAAC,IAAI,EAAE;IACjD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,GAAG,MAAM,CAKT"}