@agent-native/core 0.109.2 → 0.109.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 (97) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +8 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/create.ts +18 -12
  5. package/corpus/core/src/client/require-session.tsx +19 -3
  6. package/corpus/core/src/client/use-action.ts +145 -2
  7. package/corpus/core/src/client/use-session.ts +79 -48
  8. package/corpus/core/src/db/client.ts +13 -7
  9. package/corpus/core/src/db/create-get-db.ts +20 -13
  10. package/corpus/core/src/db/request-telemetry.ts +154 -0
  11. package/corpus/core/src/integrations/index.ts +1 -0
  12. package/corpus/core/src/integrations/plugin.ts +9 -3
  13. package/corpus/core/src/integrations/remote-push-delivery-job.ts +48 -0
  14. package/corpus/core/src/integrations/remote-push-delivery.ts +370 -0
  15. package/corpus/core/src/integrations/remote-push-store.ts +275 -26
  16. package/corpus/core/src/server/better-auth-instance.ts +11 -3
  17. package/corpus/core/src/server/core-routes-plugin.ts +36 -14
  18. package/corpus/core/src/server/framework-request-handler.ts +11 -1
  19. package/corpus/core/src/server/http-response-telemetry.ts +233 -31
  20. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
  21. package/corpus/templates/clips/actions/finalize-recording.ts +3 -1
  22. package/corpus/templates/clips/actions/set-mobile-capture-state.ts +4 -1
  23. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -6
  24. package/corpus/templates/clips/changelog/2026-07-18-new-recording-is-now-emphasized-as-the-primary-action-in-the.md +6 -0
  25. package/corpus/templates/clips/changelog/2026-07-18-recordings-now-remain-safely-recoverable-until-the-stored-me.md +6 -0
  26. package/corpus/templates/tasks/AGENTS.md +3 -0
  27. package/corpus/templates/tasks/actions/render-task-list-inline.ts +202 -0
  28. package/corpus/templates/tasks/changelog/2026-07-18-task-lists-can-now-appear-as-interactive-widgets-inline-in-c.md +6 -0
  29. package/corpus/templates/tasks/docs/features/f8-chat.mdx +2 -0
  30. package/corpus/templates/tasks/server/plugins/agent-chat.ts +2 -0
  31. package/dist/cli/create.d.ts +10 -1
  32. package/dist/cli/create.d.ts.map +1 -1
  33. package/dist/cli/create.js +17 -13
  34. package/dist/cli/create.js.map +1 -1
  35. package/dist/client/require-session.d.ts.map +1 -1
  36. package/dist/client/require-session.js +6 -3
  37. package/dist/client/require-session.js.map +1 -1
  38. package/dist/client/use-action.d.ts.map +1 -1
  39. package/dist/client/use-action.js +117 -1
  40. package/dist/client/use-action.js.map +1 -1
  41. package/dist/client/use-session.d.ts.map +1 -1
  42. package/dist/client/use-session.js +72 -50
  43. package/dist/client/use-session.js.map +1 -1
  44. package/dist/collab/routes.d.ts +1 -1
  45. package/dist/collab/struct-routes.d.ts +1 -1
  46. package/dist/db/client.d.ts.map +1 -1
  47. package/dist/db/client.js +7 -1
  48. package/dist/db/client.js.map +1 -1
  49. package/dist/db/create-get-db.d.ts +3 -6
  50. package/dist/db/create-get-db.d.ts.map +1 -1
  51. package/dist/db/create-get-db.js +9 -2
  52. package/dist/db/create-get-db.js.map +1 -1
  53. package/dist/db/request-telemetry.d.ts +23 -0
  54. package/dist/db/request-telemetry.d.ts.map +1 -0
  55. package/dist/db/request-telemetry.js +95 -0
  56. package/dist/db/request-telemetry.js.map +1 -0
  57. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  58. package/dist/integrations/index.d.ts +1 -0
  59. package/dist/integrations/index.d.ts.map +1 -1
  60. package/dist/integrations/index.js +1 -0
  61. package/dist/integrations/index.js.map +1 -1
  62. package/dist/integrations/plugin.d.ts.map +1 -1
  63. package/dist/integrations/plugin.js +8 -3
  64. package/dist/integrations/plugin.js.map +1 -1
  65. package/dist/integrations/remote-push-delivery-job.d.ts +4 -0
  66. package/dist/integrations/remote-push-delivery-job.d.ts.map +1 -0
  67. package/dist/integrations/remote-push-delivery-job.js +44 -0
  68. package/dist/integrations/remote-push-delivery-job.js.map +1 -0
  69. package/dist/integrations/remote-push-delivery.d.ts +11 -0
  70. package/dist/integrations/remote-push-delivery.d.ts.map +1 -0
  71. package/dist/integrations/remote-push-delivery.js +265 -0
  72. package/dist/integrations/remote-push-delivery.js.map +1 -0
  73. package/dist/integrations/remote-push-store.d.ts +33 -0
  74. package/dist/integrations/remote-push-store.d.ts.map +1 -1
  75. package/dist/integrations/remote-push-store.js +195 -26
  76. package/dist/integrations/remote-push-store.js.map +1 -1
  77. package/dist/progress/routes.d.ts +1 -1
  78. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  79. package/dist/resources/handlers.d.ts +3 -3
  80. package/dist/secrets/routes.d.ts +9 -9
  81. package/dist/server/better-auth-instance.js +7 -3
  82. package/dist/server/better-auth-instance.js.map +1 -1
  83. package/dist/server/core-routes-plugin.d.ts +8 -0
  84. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  85. package/dist/server/core-routes-plugin.js +23 -8
  86. package/dist/server/core-routes-plugin.js.map +1 -1
  87. package/dist/server/framework-request-handler.d.ts.map +1 -1
  88. package/dist/server/framework-request-handler.js +9 -1
  89. package/dist/server/framework-request-handler.js.map +1 -1
  90. package/dist/server/http-response-telemetry.d.ts +3 -2
  91. package/dist/server/http-response-telemetry.d.ts.map +1 -1
  92. package/dist/server/http-response-telemetry.js +160 -27
  93. package/dist/server/http-response-telemetry.js.map +1 -1
  94. package/dist/server/transcribe-voice.d.ts +1 -1
  95. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
  96. package/package.json +1 -1
  97. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
@@ -1,18 +1,58 @@
1
- import { defineEventHandler, getHeader, getMethod } from "h3";
2
- import type { EventHandler, H3Event } from "h3";
1
+ import { randomUUID } from "node:crypto";
3
2
 
3
+ import {
4
+ getHeader,
5
+ getMethod,
6
+ getResponseStatus,
7
+ setResponseHeader,
8
+ setServerTiming,
9
+ } from "h3";
10
+ import type { H3Event } from "h3";
11
+
12
+ import {
13
+ claimStartupDatabaseTelemetry,
14
+ createDatabaseRequestTelemetry,
15
+ enterDatabaseRequestTelemetry,
16
+ type DatabaseRequestTelemetry,
17
+ } from "../db/request-telemetry.js";
4
18
  import { getDatabaseRuntimeFingerprint } from "../db/runtime-diagnostics.js";
5
19
  import { isMcpPublicPath } from "../mcp/route-paths.js";
6
20
  import { track } from "../tracking/index.js";
7
21
  import { getAppName } from "./app-name.js";
8
22
 
9
23
  const TELEMETRY_EVENT_NAME = "http.response";
24
+ const REQUEST_ID_HEADER = "x-agent-native-request-id";
10
25
  const TRACKING_INGEST_PATHS = new Set([
11
26
  "/track",
12
27
  "/api/analytics/track",
13
28
  "/api/events/track",
14
29
  "/_agent-native/track",
15
30
  ]);
31
+ const PROCESS_STATE_KEY = Symbol.for(
32
+ "@agent-native/core/http-response-telemetry.process-state",
33
+ );
34
+ type ProcessTelemetryState = { requestSequence: number };
35
+ type GlobalWithProcessTelemetry = typeof globalThis & {
36
+ [PROCESS_STATE_KEY]?: ProcessTelemetryState;
37
+ };
38
+ const globalRef = globalThis as GlobalWithProcessTelemetry;
39
+ const processState =
40
+ globalRef[PROCESS_STATE_KEY] ??
41
+ (globalRef[PROCESS_STATE_KEY] = { requestSequence: 0 });
42
+ const REQUEST_TELEMETRY_KEY = Symbol.for(
43
+ "@agent-native/core/http-response-telemetry.request",
44
+ );
45
+ const installedApps = new WeakSet<object>();
46
+
47
+ interface HttpRequestTelemetryState {
48
+ startedAt: number;
49
+ requestId: string;
50
+ processAgeAtStartMs: number;
51
+ requestSequence: number;
52
+ frameworkReadyWaitMs: number;
53
+ db: DatabaseRequestTelemetry;
54
+ startupDb?: DatabaseRequestTelemetry;
55
+ }
16
56
 
17
57
  function envValue(key: string): string | undefined {
18
58
  const value = process.env[key]?.trim();
@@ -27,9 +67,9 @@ function boolEnv(key: string): boolean {
27
67
 
28
68
  function sampleRate(): number {
29
69
  const raw = envValue("AGENT_NATIVE_HTTP_TELEMETRY_SAMPLE_RATE");
30
- if (!raw) return 1;
70
+ if (!raw) return 0.1;
31
71
  const parsed = Number.parseFloat(raw);
32
- if (!Number.isFinite(parsed)) return 1;
72
+ if (!Number.isFinite(parsed)) return 0.1;
33
73
  return Math.max(0, Math.min(1, parsed));
34
74
  }
35
75
 
@@ -107,31 +147,63 @@ function organizationForHost(host: string | undefined): string | undefined {
107
147
  : undefined;
108
148
  }
109
149
 
110
- function shouldTrack(pathname: string, statusCode: number): boolean {
150
+ function shouldTrack(
151
+ pathname: string,
152
+ statusCode: number,
153
+ state: HttpRequestTelemetryState,
154
+ ): boolean {
111
155
  if (shouldDisableTelemetry()) return false;
112
156
  if (TRACKING_INGEST_PATHS.has(pathname)) return false;
113
157
  if (pathname.startsWith("/api/analytics/replay")) return false;
114
158
  if (statusCode >= 500) return true;
159
+ if (
160
+ statusCode >= 400 &&
161
+ statusCode < 500 &&
162
+ /(?:^|\/)_agent-native\/actions(?:\/|$)/.test(pathname)
163
+ ) {
164
+ return true;
165
+ }
166
+ if (state.requestSequence === 1) return true;
167
+ if (state.startupDb) return true;
168
+ if (Date.now() - state.startedAt >= 1_000) return true;
169
+ if (state.db.errorCount > 0 || state.db.timeoutCount > 0) {
170
+ return true;
171
+ }
115
172
  const rate = sampleRate();
116
173
  if (rate <= 0) return false;
117
174
  if (rate >= 1) return true;
118
175
  return Math.random() < rate;
119
176
  }
120
177
 
121
- function responseStatusCode(event: H3Event): number {
178
+ function responseStatusCode(event: H3Event, response?: Response): number {
122
179
  const raw =
180
+ response?.status ??
123
181
  (event.node?.res as any)?.statusCode ??
124
182
  (event.node?.res as any)?.status ??
125
- (event as any).res?.status ??
183
+ getResponseStatus(event) ??
126
184
  200;
127
185
  const parsed = Number(raw);
128
186
  return Number.isFinite(parsed) ? parsed : 200;
129
187
  }
130
188
 
131
- function emitTelemetry(event: H3Event, startedAt: number): void {
132
- const statusCode = responseStatusCode(event);
189
+ function runtimeProvider(): string {
190
+ if (process.env.NETLIFY) return "netlify";
191
+ if (process.env.VERCEL) return "vercel";
192
+ if (process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.LAMBDA_TASK_ROOT) {
193
+ return "aws-lambda";
194
+ }
195
+ if (process.env.CF_PAGES) return "cloudflare-pages";
196
+ return "node";
197
+ }
198
+
199
+ function emitTelemetry(
200
+ event: H3Event,
201
+ state: HttpRequestTelemetryState,
202
+ response?: Response,
203
+ ): void {
204
+ const statusCode = responseStatusCode(event, response);
133
205
  const pathname = requestPath(event);
134
- if (!shouldTrack(pathname, statusCode)) return;
206
+ if (!shouldTrack(pathname, statusCode, state)) return;
135
207
 
136
208
  try {
137
209
  const host = hostForEvent(event);
@@ -146,7 +218,17 @@ function emitTelemetry(event: H3Event, startedAt: number): void {
146
218
  route_kind: routeKind(pathname),
147
219
  status_code: statusCode,
148
220
  status_class: statusClass(statusCode),
149
- duration_ms: Math.max(0, Date.now() - startedAt),
221
+ duration_ms: Math.max(0, Date.now() - state.startedAt),
222
+ request_id: state.requestId,
223
+ measurement: "nitro_request",
224
+ cold_start: state.requestSequence === 1,
225
+ request_sequence: state.requestSequence,
226
+ process_age_ms: state.processAgeAtStartMs,
227
+ framework_ready_wait_ms: state.frameworkReadyWaitMs,
228
+ runtime_provider: runtimeProvider(),
229
+ function_name: envValue("AWS_LAMBDA_FUNCTION_NAME"),
230
+ function_memory_mb: envValue("AWS_LAMBDA_FUNCTION_MEMORY_SIZE"),
231
+ region: envValue("AWS_REGION") ?? envValue("VERCEL_REGION"),
150
232
  host,
151
233
  environment: envValue("NODE_ENV"),
152
234
  deploy_context: envValue("CONTEXT") ?? envValue("VERCEL_ENV"),
@@ -161,34 +243,154 @@ function emitTelemetry(event: H3Event, startedAt: number): void {
161
243
  db_url_hash: db.urlHash,
162
244
  db_neon_endpoint: db.neon?.endpointId,
163
245
  db_neon_pooled: db.neon?.pooled,
246
+ db_operation_count: state.db.operationCount,
247
+ db_query_count: state.db.queryCount,
248
+ db_connect_count: state.db.connectCount,
249
+ db_retry_count: state.db.retryCount,
250
+ db_error_count: state.db.errorCount,
251
+ db_timeout_count: state.db.timeoutCount,
252
+ db_operation_total_ms: Math.round(state.db.operationTotalMs),
253
+ db_operation_wall_ms: Math.round(state.db.operationWallMs),
254
+ db_query_total_ms: Math.round(state.db.queryTotalMs),
255
+ db_connect_total_ms: Math.round(state.db.connectTotalMs),
256
+ db_slowest_operation_ms: Math.round(state.db.slowestOperationMs),
257
+ startup_db_operation_count: state.startupDb?.operationCount,
258
+ startup_db_query_count: state.startupDb?.queryCount,
259
+ startup_db_connect_count: state.startupDb?.connectCount,
260
+ startup_db_retry_count: state.startupDb?.retryCount,
261
+ startup_db_error_count: state.startupDb?.errorCount,
262
+ startup_db_timeout_count: state.startupDb?.timeoutCount,
263
+ startup_db_operation_total_ms: state.startupDb
264
+ ? Math.round(state.startupDb.operationTotalMs)
265
+ : undefined,
266
+ startup_db_operation_wall_ms: state.startupDb
267
+ ? Math.round(state.startupDb.operationWallMs)
268
+ : undefined,
269
+ startup_db_query_total_ms: state.startupDb
270
+ ? Math.round(state.startupDb.queryTotalMs)
271
+ : undefined,
272
+ startup_db_connect_total_ms: state.startupDb
273
+ ? Math.round(state.startupDb.connectTotalMs)
274
+ : undefined,
275
+ startup_db_slowest_operation_ms: state.startupDb
276
+ ? Math.round(state.startupDb.slowestOperationMs)
277
+ : undefined,
164
278
  });
165
279
  } catch {
166
280
  // Response telemetry is best-effort. Never perturb request handling.
167
281
  }
168
282
  }
169
283
 
170
- export function createHttpResponseTelemetryMiddleware(): EventHandler {
171
- return defineEventHandler((event) => {
172
- const startedAt = Date.now();
173
- const res = event.node?.res as
174
- | {
175
- once?: (event: "finish" | "close", cb: () => void) => void;
176
- on?: (event: "finish" | "close", cb: () => void) => void;
177
- }
178
- | undefined;
179
- let emitted = false;
180
- const emitOnce = () => {
181
- if (emitted) return;
182
- emitted = true;
183
- emitTelemetry(event, startedAt);
284
+ function requestTelemetryState(
285
+ event: H3Event,
286
+ ): HttpRequestTelemetryState | undefined {
287
+ return (event.context as Record<PropertyKey, unknown>)[
288
+ REQUEST_TELEMETRY_KEY
289
+ ] as HttpRequestTelemetryState | undefined;
290
+ }
291
+
292
+ function appendServerTiming(
293
+ response: Response,
294
+ event: H3Event,
295
+ name: string,
296
+ durationMs: number,
297
+ ): void {
298
+ const duration = Math.max(0, Math.round(durationMs));
299
+ try {
300
+ response.headers.append("server-timing", `${name};dur=${duration}`);
301
+ } catch {
302
+ try {
303
+ setServerTiming(event, name, { dur: duration });
304
+ } catch {
305
+ // Some adapters finalize headers eagerly. Tracking still runs.
306
+ }
307
+ }
308
+ }
309
+
310
+ export function recordFrameworkReadyWait(
311
+ event: H3Event,
312
+ durationMs: number,
313
+ ): void {
314
+ const state = requestTelemetryState(event);
315
+ if (state) {
316
+ state.frameworkReadyWaitMs += Math.max(0, durationMs);
317
+ state.startupDb ??= claimStartupDatabaseTelemetry();
318
+ }
319
+ }
320
+
321
+ export function installHttpResponseTelemetryHooks(nitroApp: any): void {
322
+ if (!nitroApp || installedApps.has(nitroApp)) return;
323
+ const hooks = nitroApp.hooks;
324
+ if (!hooks?.hook) return;
325
+ installedApps.add(nitroApp);
326
+
327
+ hooks.hook("request", (event: H3Event) => {
328
+ const state: HttpRequestTelemetryState = {
329
+ startedAt: Date.now(),
330
+ requestId: randomUUID(),
331
+ processAgeAtStartMs: Math.max(0, Math.round(process.uptime() * 1_000)),
332
+ requestSequence: ++processState.requestSequence,
333
+ frameworkReadyWaitMs: 0,
334
+ db: createDatabaseRequestTelemetry(),
184
335
  };
336
+ (event.context as Record<PropertyKey, unknown>)[REQUEST_TELEMETRY_KEY] =
337
+ state;
338
+ enterDatabaseRequestTelemetry(state.db);
339
+ });
185
340
 
186
- if (typeof res?.once === "function") {
187
- res.once("finish", emitOnce);
188
- res.once("close", emitOnce);
189
- } else if (typeof res?.on === "function") {
190
- res.on("finish", emitOnce);
191
- res.on("close", emitOnce);
341
+ hooks.hook("response", (response: Response, event: H3Event) => {
342
+ const state = requestTelemetryState(event);
343
+ if (!state) return;
344
+
345
+ const durationMs = Math.max(0, Date.now() - state.startedAt);
346
+ try {
347
+ response.headers.set(REQUEST_ID_HEADER, state.requestId);
348
+ } catch {
349
+ try {
350
+ setResponseHeader(event, REQUEST_ID_HEADER, state.requestId);
351
+ } catch {
352
+ // Some adapters finalize headers eagerly. Tracking still has the id.
353
+ }
354
+ }
355
+ appendServerTiming(response, event, "app", durationMs);
356
+ if (state.frameworkReadyWaitMs > 0) {
357
+ appendServerTiming(
358
+ response,
359
+ event,
360
+ "startup",
361
+ state.frameworkReadyWaitMs,
362
+ );
192
363
  }
364
+ if (state.db.operationCount > 0) {
365
+ appendServerTiming(response, event, "db", state.db.operationWallMs);
366
+ appendServerTiming(
367
+ response,
368
+ event,
369
+ "db-connect",
370
+ state.db.connectTotalMs,
371
+ );
372
+ appendServerTiming(
373
+ response,
374
+ event,
375
+ "db-slowest",
376
+ state.db.slowestOperationMs,
377
+ );
378
+ }
379
+ if (state.startupDb && state.startupDb.operationCount > 0) {
380
+ appendServerTiming(
381
+ response,
382
+ event,
383
+ "startup-db",
384
+ state.startupDb.operationWallMs,
385
+ );
386
+ appendServerTiming(
387
+ response,
388
+ event,
389
+ "startup-db-connect",
390
+ state.startupDb.connectTotalMs,
391
+ );
392
+ }
393
+
394
+ emitTelemetry(event, state, response);
193
395
  });
194
396
  }
@@ -138,6 +138,25 @@ Attribution parsing is fully defensive and never blocks signup — a missing/mal
138
138
  Other framework-level baseline events:
139
139
 
140
140
  - `session status` from `useSession()`, with `signed_in`
141
+ - `action.response` from the browser action transport, with action name,
142
+ browser-perceived duration and TTFB, response status/outcome, response size
143
+ when known, and parsed `Server-Timing` phases for framework readiness and
144
+ database work. Its `request_id` joins the exact browser and server events.
145
+ This separates server time from CDN/network/body overhead.
146
+ - `http.response` from Nitro request/response hooks, with normalized path,
147
+ status, request duration, first-request-in-isolate cold marker, process age,
148
+ framework readiness wait, deploy/runtime fingerprint, database
149
+ connection/query counts and timings, retries, timeouts, and failures. It also
150
+ emits `Server-Timing` for `app`, `startup`, `db`, `db-connect`, and
151
+ `db-slowest` plus an `X-Agent-Native-Request-Id` correlation header where
152
+ applicable. Query text and parameters are never captured.
153
+ Database activity that begins during the first two minutes of process/plugin
154
+ initialization is reported separately as `startup_db_*` on the first
155
+ framework request that passes the readiness gate.
156
+ Slow, cold-isolate, server failures, and 4xx action routes are always
157
+ retained; fast successful requests default to 10% sampling. Override with
158
+ `AGENT_NATIVE_HTTP_TELEMETRY_SAMPLE_RATE` on the server and
159
+ `VITE_AGENT_NATIVE_ACTION_TELEMETRY_SAMPLE_RATE` in the browser.
141
160
  - `signup` from Better Auth user creation, with `auth_provider`, `auth_user_id`, and first-touch referral attribution (`referral_source`, `referrer_user`, `referral_medium`, `referral_campaign`, `utm_*`, `first_touch_path`, `landing_referrer` — see "Referral / viral attribution" above)
142
161
  - `builder connect clicked` and `builder connect popup blocked` from browser Connect Builder CTAs
143
162
  - `builder connect started`, `builder connect succeeded`, `builder connect failed`, `builder disconnect succeeded`, and `builder disconnect failed` from the Builder connection routes, with LLM connection context when resolvable
@@ -204,7 +204,9 @@ async function verifyServedMediaUrl(videoUrl: string): Promise<number | null> {
204
204
  const statusOk = response.status === 200 || response.status === 206;
205
205
  if (statusOk) {
206
206
  const servedBytes = servedMediaSizeBytes(response);
207
- if (await responseHasReadableMediaBytes(response)) {
207
+ if (servedBytes === null) {
208
+ lastFailure = "Stored media byte count could not be verified";
209
+ } else if (await responseHasReadableMediaBytes(response)) {
208
210
  // Builder stable video URLs intentionally replace the source object
209
211
  // with a smaller compressed generation at the same URL. A positive,
210
212
  // readable object is the invariant here; source-byte equality is
@@ -6,7 +6,7 @@ export const MOBILE_CAPTURE_STATE_KEY = "mobile-capture-state";
6
6
 
7
7
  const mobileCaptureStateSchema = z
8
8
  .object({
9
- view: z.enum(["home", "dictate", "meeting", "video"]),
9
+ view: z.enum(["home", "dictate", "meeting", "video", "clips"]),
10
10
  phase: z.enum([
11
11
  "idle",
12
12
  "ready",
@@ -15,9 +15,12 @@ const mobileCaptureStateSchema = z
15
15
  "saving",
16
16
  "processing",
17
17
  "review",
18
+ "browsing",
19
+ "playing",
18
20
  "error",
19
21
  ]),
20
22
  captureId: z.string().trim().min(1).max(200).optional(),
23
+ recordingId: z.string().trim().min(1).max(200).optional(),
21
24
  })
22
25
  .strict();
23
26
 
@@ -384,12 +384,7 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
384
384
  ) : (
385
385
  <>
386
386
  <div className="px-3 py-3">
387
- <Button
388
- className="w-full gap-1.5"
389
- variant="outline"
390
- size="sm"
391
- asChild
392
- >
387
+ <Button className="w-full gap-1.5" size="sm" asChild>
393
388
  <NavLink to="/record">
394
389
  <IconPlayerRecord className="h-4 w-4" />
395
390
  {t("navigation.newRecording")}
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-18
4
+ ---
5
+
6
+ New recording is now emphasized as the primary action in the library sidebar.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-18
4
+ ---
5
+
6
+ Recordings now remain safely recoverable until the stored media size is verified.
@@ -38,6 +38,7 @@ Tasks is a task-list-first agent-native app. The task list at `/tasks` is the de
38
38
  | `update-visible-task-fields` | POST | Replace which custom fields appear on task cards (max 3) |
39
39
  | `view-screen` | — | Read navigation, UI bulk selection, visible tasks, and inbox snapshot |
40
40
  | `navigate` | — | Move UI to a view: `tasks`, `inbox`, `fields`, `extensions`, `team` (`home`/`ask` → `tasks`) |
41
+ | `render-task-list-inline` | — | Render an interactive task-list widget inline in chat without leaving the current view |
41
42
 
42
43
  ## Store Functions And Transactions
43
44
 
@@ -128,6 +129,8 @@ Default navigation shape on `/tasks`:
128
129
  - Capture in chat → `create-inbox-item` by default; use `create-task` only when the user asks to add directly to the task list.
129
130
  - Call `view-screen` before ambiguous edits when the user says "this task", "these tasks", "this inbox item", or "the list".
130
131
  - On `/tasks` or `/inbox`, `view-screen` returns `list` (with `items`), optional `selectedItem` (`inListSnapshot`), and optional `selection` (`selectedItems`, `selectedIdsNotInVisibleList`) when bulk-select is active.
132
+ - When the user asks to see, review, or manage tasks while `navigation.view` is not `tasks`, call `render-task-list-inline` instead of navigating away. Pass `includeDone: true` when completed tasks should be included. The widget can add tasks and toggle completion through the existing task actions.
133
+ - When the user is already on `/tasks`, use `view-screen` and the native task list for task-list context unless the user explicitly asks for an inline widget.
131
134
  - Prefer `selection.selectedItems` when the user has UI rows selected; fall back to `selectedItem` for a single deep-link highlight.
132
135
  - `delete-task`, `bulk-delete-tasks`, and `delete-inbox-item` only after explicit user confirmation in chat.
133
136
  - Use `navigation.includeDone` and `list` from `view-screen` to match what the user sees on `/tasks`.
@@ -0,0 +1,202 @@
1
+ import {
2
+ ACTION_CHAT_UI_INLINE_EXTENSION_RENDERER,
3
+ defineAction,
4
+ } from "@agent-native/core";
5
+ import { z } from "zod";
6
+
7
+ export const TASK_LIST_INLINE_CONTENT = `
8
+ <script>
9
+ document.addEventListener("alpine:init", () => {
10
+ Alpine.data("taskListWidget", () => ({
11
+ tasks: [],
12
+ draft: "",
13
+ includeDone: false,
14
+ isLoading: true,
15
+ isSaving: false,
16
+ pending: {},
17
+ error: "",
18
+ hasCompletedTasks: false,
19
+ init() {
20
+ this.includeDone = Boolean(window.slotContext?.includeDone);
21
+ window.onSlotContext?.((context) => {
22
+ if (context?.includeDone === undefined) return;
23
+ this.includeDone = Boolean(context.includeDone);
24
+ void this.refresh();
25
+ });
26
+ void this.refresh();
27
+ },
28
+ parseResult(result) {
29
+ if (typeof result === "string") {
30
+ try {
31
+ return JSON.parse(result);
32
+ } catch {
33
+ return {};
34
+ }
35
+ }
36
+ return result && typeof result === "object" ? result : {};
37
+ },
38
+ async refresh() {
39
+ this.isLoading = true;
40
+ this.error = "";
41
+ try {
42
+ const data = this.parseResult(
43
+ await appAction("list-tasks", { includeDone: this.includeDone }),
44
+ );
45
+ this.tasks = Array.isArray(data.tasks) ? data.tasks : [];
46
+ this.hasCompletedTasks = data.hasCompletedTasks === true;
47
+ } catch (error) {
48
+ this.error = error?.message || "Could not load tasks.";
49
+ } finally {
50
+ this.isLoading = false;
51
+ }
52
+ },
53
+ summaryLabel() {
54
+ if (this.isLoading) return "Loading your task list…";
55
+ const remaining = this.tasks.filter((task) => !task.done).length;
56
+ return this.includeDone
57
+ ? this.tasks.length + " shown · " + remaining + " remaining"
58
+ : remaining + " remaining";
59
+ },
60
+ setIncludeDone(value) {
61
+ this.includeDone = Boolean(value);
62
+ void this.refresh();
63
+ },
64
+ async toggle(task, done) {
65
+ const previous = task.done;
66
+ task.done = Boolean(done);
67
+ this.pending[task.id] = true;
68
+ this.error = "";
69
+ try {
70
+ await appAction("update-task", { taskId: task.id, done: task.done });
71
+ if (!this.includeDone && task.done) {
72
+ this.tasks = this.tasks.filter((item) => item.id !== task.id);
73
+ }
74
+ this.hasCompletedTasks = this.hasCompletedTasks || task.done;
75
+ } catch (error) {
76
+ task.done = previous;
77
+ this.error = error?.message || "Could not update that task.";
78
+ } finally {
79
+ delete this.pending[task.id];
80
+ }
81
+ },
82
+ async addTask() {
83
+ const title = this.draft.trim();
84
+ if (!title || this.isSaving) return;
85
+ this.isSaving = true;
86
+ this.error = "";
87
+ try {
88
+ await appAction("create-task", { title });
89
+ this.draft = "";
90
+ await this.refresh();
91
+ } catch (error) {
92
+ this.error = error?.message || "Could not add that task.";
93
+ } finally {
94
+ this.isSaving = false;
95
+ }
96
+ },
97
+ }));
98
+ });
99
+ </script>
100
+
101
+ <div x-data="taskListWidget" class="w-full max-w-xl rounded-xl border border-border bg-card p-4 text-card-foreground shadow-sm">
102
+ <div class="flex items-start justify-between gap-3">
103
+ <div class="min-w-0">
104
+ <h2 class="text-sm font-semibold">Tasks</h2>
105
+ <p class="mt-1 text-xs text-muted-foreground" x-text="summaryLabel()"></p>
106
+ </div>
107
+ <button
108
+ type="button"
109
+ class="shrink-0 rounded-md px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
110
+ @click="refresh()"
111
+ :disabled="isLoading"
112
+ >Refresh</button>
113
+ </div>
114
+
115
+ <form class="mt-4 flex gap-2" @submit.prevent="addTask()">
116
+ <input
117
+ x-model="draft"
118
+ class="min-w-0 flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring"
119
+ aria-label="New task"
120
+ placeholder="Add a task"
121
+ :disabled="isSaving"
122
+ />
123
+ <button
124
+ type="submit"
125
+ class="rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
126
+ :disabled="isSaving || !draft.trim()"
127
+ >Add</button>
128
+ </form>
129
+
130
+ <label class="mt-3 flex items-center gap-2 text-xs text-muted-foreground">
131
+ <input
132
+ type="checkbox"
133
+ class="size-3.5 accent-primary"
134
+ :checked="includeDone"
135
+ @change="setIncludeDone($event.target.checked)"
136
+ />
137
+ <span>Show completed</span>
138
+ </label>
139
+
140
+ <div x-show="error" class="mt-3 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive" role="alert">
141
+ <span x-text="error"></span>
142
+ </div>
143
+
144
+ <div x-show="isLoading" class="mt-4 space-y-2" aria-label="Loading tasks">
145
+ <div class="h-10 animate-pulse rounded-md bg-muted"></div>
146
+ <div class="h-10 animate-pulse rounded-md bg-muted"></div>
147
+ </div>
148
+
149
+ <div x-show="!isLoading && tasks.length === 0" class="mt-5 rounded-lg border border-dashed border-border px-3 py-5 text-center">
150
+ <p class="text-sm font-medium" x-text="hasCompletedTasks && !includeDone ? 'All tasks complete' : 'No tasks yet'"></p>
151
+ <p class="mt-1 text-xs text-muted-foreground" x-text="hasCompletedTasks && !includeDone ? 'Show completed to review them.' : 'Add a task above to get started.'"></p>
152
+ </div>
153
+
154
+ <ul x-show="!isLoading && tasks.length > 0" class="mt-4 divide-y divide-border rounded-lg border border-border" aria-label="Tasks list">
155
+ <template x-for="task in tasks" :key="task.id">
156
+ <li class="flex items-center gap-3 px-3 py-3" :class="task.done ? 'text-muted-foreground' : ''">
157
+ <input
158
+ type="checkbox"
159
+ class="size-4 shrink-0 accent-primary"
160
+ :checked="task.done"
161
+ :disabled="pending[task.id]"
162
+ @change="toggle(task, $event.target.checked)"
163
+ :aria-label="task.done ? 'Mark ' + task.title + ' incomplete' : 'Mark ' + task.title + ' complete'"
164
+ />
165
+ <span class="min-w-0 flex-1 break-words text-sm" :class="task.done ? 'line-through' : ''" x-text="task.title"></span>
166
+ </li>
167
+ </template>
168
+ </ul>
169
+ </div>
170
+ `;
171
+
172
+ export default defineAction({
173
+ description:
174
+ "Render the current task list as an interactive widget inline in chat without navigating away from the current page. Use when the user asks to see, review, or manage tasks while they are not on /tasks.",
175
+ schema: z.object({
176
+ includeDone: z
177
+ .boolean()
178
+ .default(false)
179
+ .describe("When true, show completed tasks in the widget."),
180
+ }),
181
+ http: false,
182
+ readOnly: true,
183
+ chatUI: {
184
+ renderer: ACTION_CHAT_UI_INLINE_EXTENSION_RENDERER,
185
+ title: "Task list",
186
+ description: "Render the task list inline in the conversation.",
187
+ },
188
+ run: async (args) => ({
189
+ ok: true,
190
+ inlineExtension: {
191
+ mode: "transient" as const,
192
+ id: `tasks-inline-${Date.now().toString(36)}-${Math.random()
193
+ .toString(36)
194
+ .slice(2, 8)}`,
195
+ name: "Task list",
196
+ description: args.includeDone ? "Open and completed tasks" : "Open tasks",
197
+ content: TASK_LIST_INLINE_CONTENT,
198
+ context: { includeDone: args.includeDone },
199
+ initialHeight: 360,
200
+ },
201
+ }),
202
+ });
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-18
4
+ ---
5
+
6
+ Task lists can now appear as interactive widgets inline in chat without leaving the current page.
@@ -24,6 +24,7 @@ The **chat** is a first-class surface of the Tasks app — not a side panel bolt
24
24
  - **Triage inbox** — list, edit, delete, reorder, and **mark ready** ([F4](./f4-inbox.mdx)).
25
25
  - **Filter** the task list the same way as the UI (e.g. hide completed tasks).
26
26
  - **Ask what's on screen** — the agent can read the user's current view and selection before acting.
27
+ - **Keep the current page open while reviewing tasks** — when the user is elsewhere in the app, the agent can show an interactive task-list widget inline in chat.
27
28
 
28
29
  <!-- status: todo -->
29
30
 
@@ -51,6 +52,7 @@ Every UI capability maps to agent tools (actions). The agent uses these — not
51
52
  | **[F2 Custom fields](./f2-custom-fields.mdx)** | Create, rename, configure options; set field values; delete fields |
52
53
  | **Navigation** | Open inbox, main task page, or a specific task via `navigate` |
53
54
  | **Context** | Read current screen and bulk `selection` via `view-screen` |
55
+ | **Inline task list** | Render the task list in chat with `render-task-list-inline` when the user is not on `/tasks` |
54
56
 
55
57
  <!-- status: todo -->
56
58