@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.
- package/dist/actions/delete-staged-dataset.d.ts +3 -1
- package/dist/actions/delete-staged-dataset.js +3 -26
- package/dist/actions/delete-staged-dataset.js.map +1 -1
- package/dist/actions/delete-workspace-resource.d.ts +13 -13
- package/dist/actions/list-staged-datasets.d.ts +1 -1
- package/dist/actions/list-staged-datasets.js +3 -28
- package/dist/actions/list-staged-datasets.js.map +1 -1
- package/dist/actions/provider-api-catalog.d.ts +6 -4
- package/dist/actions/provider-api-catalog.js +3 -17
- package/dist/actions/provider-api-catalog.js.map +1 -1
- package/dist/actions/provider-api-docs.d.ts +21 -2
- package/dist/actions/provider-api-docs.js +6 -51
- package/dist/actions/provider-api-docs.js.map +1 -1
- package/dist/actions/provider-api-register.d.ts +39 -15
- package/dist/actions/provider-api-register.js +4 -193
- package/dist/actions/provider-api-register.js.map +1 -1
- package/dist/actions/provider-api-request.d.ts +34 -2
- package/dist/actions/provider-api-request.js +6 -171
- package/dist/actions/provider-api-request.js.map +1 -1
- package/dist/actions/query-staged-dataset.d.ts +18 -1
- package/dist/actions/query-staged-dataset.d.ts.map +1 -1
- package/dist/actions/query-staged-dataset.js +3 -100
- package/dist/actions/query-staged-dataset.js.map +1 -1
- package/dist/actions/upsert-destination.d.ts +12 -12
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +26 -64
- package/dist/components/layout/Layout.js.map +1 -1
- package/package.json +4 -3
- package/src/actions/delete-staged-dataset.ts +3 -33
- package/src/actions/list-staged-datasets.ts +3 -30
- package/src/actions/provider-api-audit.spec.ts +2 -3
- package/src/actions/provider-api-catalog.ts +10 -23
- package/src/actions/provider-api-docs.ts +19 -68
- package/src/actions/provider-api-register.ts +5 -233
- package/src/actions/provider-api-request.ts +19 -232
- package/src/actions/query-staged-dataset.ts +3 -119
- package/src/components/layout/Layout.spec.tsx +7 -11
- package/src/components/layout/Layout.tsx +42 -145
- package/dist/actions/provider-api-audit.d.ts +0 -7
- package/dist/actions/provider-api-audit.d.ts.map +0 -1
- package/dist/actions/provider-api-audit.js +0 -74
- package/dist/actions/provider-api-audit.js.map +0 -1
- package/src/actions/provider-api-audit.ts +0 -88
|
@@ -1,241 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
13
|
-
|
|
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
|
|
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("
|
|
187
|
+
const age = [...container.querySelectorAll("span")].find(
|
|
188
188
|
(element) => element.textContent === "5m",
|
|
189
189
|
);
|
|
190
|
-
expect(age?.className).toContain("
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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.
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
(
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
)
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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 +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"}
|