@agent-native/core 0.119.4 → 0.119.5

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 (71) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +8 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/engine/index.ts +2 -0
  5. package/corpus/core/src/agent/engine/registry.ts +95 -2
  6. package/corpus/core/src/scripts/agent-engines/list-agent-engines.ts +8 -0
  7. package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +5 -1
  8. package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +9 -1
  9. package/corpus/templates/slides/actions/patch-deck.ts +2 -1
  10. package/corpus/templates/slides/changelog/2026-07-23-opening-or-refining-a-deck-no-longer-crashes-with-a-blank-er.md +6 -0
  11. package/corpus/templates/slides/shared/aspect-ratios.ts +4 -1
  12. package/corpus/templates/tasks/.oxfmtrc.json +8 -0
  13. package/corpus/templates/tasks/AGENTS.md +34 -41
  14. package/corpus/templates/tasks/README.md +13 -6
  15. package/corpus/templates/tasks/actions/bulk-delete-inbox-items.ts +10 -8
  16. package/corpus/templates/tasks/actions/bulk-delete-tasks.ts +10 -8
  17. package/corpus/templates/tasks/actions/bulk-mark-inbox-items-ready.ts +1 -1
  18. package/corpus/templates/tasks/actions/bulk-update-tasks.ts +12 -10
  19. package/corpus/templates/tasks/actions/create-inbox-item.ts +7 -4
  20. package/corpus/templates/tasks/actions/create-task.ts +2 -1
  21. package/corpus/templates/tasks/actions/delete-inbox-item.ts +6 -4
  22. package/corpus/templates/tasks/actions/delete-task.ts +1 -1
  23. package/corpus/templates/tasks/actions/list-inbox-items.ts +4 -2
  24. package/corpus/templates/tasks/actions/list-visible-task-fields.ts +3 -1
  25. package/corpus/templates/tasks/actions/mark-inbox-item-ready.ts +6 -4
  26. package/corpus/templates/tasks/actions/navigate.ts +16 -14
  27. package/corpus/templates/tasks/actions/reorder-inbox-items.ts +9 -7
  28. package/corpus/templates/tasks/actions/update-inbox-item.ts +6 -4
  29. package/corpus/templates/tasks/actions/update-task.ts +12 -9
  30. package/corpus/templates/tasks/actions/update-visible-task-fields.ts +10 -8
  31. package/corpus/templates/tasks/actions/view-screen.ts +1 -1
  32. package/corpus/templates/tasks/app/components/tasks/fields/TaskFieldsSidebar.tsx +15 -0
  33. package/corpus/templates/tasks/changelog/2026-07-23-task-detail-extensions.md +6 -0
  34. package/corpus/templates/tasks/drizzle.config.ts +3 -0
  35. package/corpus/templates/tasks/package.json +4 -1
  36. package/corpus/templates/tasks/server/db/schema.ts +0 -10
  37. package/corpus/templates/tasks/server/db/test-tasks-table.ts +0 -5
  38. package/corpus/templates/tasks/server/plugins/agent-chat.ts +1 -1
  39. package/corpus/templates/tasks/server/plugins/db.ts +43 -10
  40. package/corpus/templates/tasks/server/user-config/store.ts +42 -81
  41. package/corpus/templates/tasks/tsconfig.json +1 -2
  42. package/corpus/templates/tasks/vitest.config.ts +10 -1
  43. package/dist/agent/engine/index.d.ts +1 -1
  44. package/dist/agent/engine/index.d.ts.map +1 -1
  45. package/dist/agent/engine/index.js +1 -1
  46. package/dist/agent/engine/index.js.map +1 -1
  47. package/dist/agent/engine/registry.d.ts +32 -1
  48. package/dist/agent/engine/registry.d.ts.map +1 -1
  49. package/dist/agent/engine/registry.js +75 -3
  50. package/dist/agent/engine/registry.js.map +1 -1
  51. package/dist/collab/routes.d.ts +1 -1
  52. package/dist/collab/struct-routes.d.ts +1 -1
  53. package/dist/notifications/routes.d.ts +1 -1
  54. package/dist/observability/routes.d.ts +1 -1
  55. package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
  56. package/dist/scripts/agent-engines/list-agent-engines.js +8 -2
  57. package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
  58. package/dist/scripts/agent-engines/manage-agent-engine.d.ts.map +1 -1
  59. package/dist/scripts/agent-engines/manage-agent-engine.js +5 -2
  60. package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
  61. package/dist/scripts/agent-engines/set-agent-engine.d.ts.map +1 -1
  62. package/dist/scripts/agent-engines/set-agent-engine.js +9 -2
  63. package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
  64. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  65. package/package.json +1 -1
  66. package/src/agent/engine/index.ts +2 -0
  67. package/src/agent/engine/registry.ts +95 -2
  68. package/src/scripts/agent-engines/list-agent-engines.ts +8 -0
  69. package/src/scripts/agent-engines/manage-agent-engine.ts +5 -1
  70. package/src/scripts/agent-engines/set-agent-engine.ts +9 -1
  71. package/corpus/templates/tasks/.prettierrc +0 -5
package/corpus/README.md CHANGED
@@ -30,4 +30,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
30
30
 
31
31
  - core files: 1660
32
32
  - toolkit files: 159
33
- - template files: 6327
33
+ - template files: 6330
@@ -1,5 +1,13 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.119.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 13eb9f7: Fix AI SDK provider engines being unusable on bundled serverless deploys, and stop rewriting custom model IDs on save/read.
8
+ - **Serverless package detection.** `isAgentEnginePackageInstalled` relied on `require.resolve`, which fails on bundled serverless runtimes (Vercel/Netlify via Nitro) where optional provider packages (`ai`, `@ai-sdk/*`) are inlined into the function bundle. Every engine-usability gate then rejected the AI SDK engines and the agent silently fell back to the native Anthropic engine. Package resolution now treats a resolve miss as available only when there is real evidence of a bundled runtime (Vercel/Netlify markers, or a bundle-output module path), and defers to the engine's own dynamic `import()` as the real gate. Generic container/Lambda/Cloud Run deploys that ship a real `node_modules` still surface genuine "package not installed" misses.
9
+ - **Custom model preservation.** `normalizeModelForEngine` replaced any unrecognized model ID with the engine default when the settings actions passed a static registry entry, so a custom OpenAI-compatible gateway model (e.g. an Ollama model) reverted to the OpenAI default on save and read. The set/list/app-default actions now resolve the OpenAI-compatible-endpoint capability (`resolveEnginePreservesCustomModels`) and pass it through, preserving custom IDs verbatim — including version-shaped IDs — while first-party OpenAI still normalizes unknown IDs to a supported model.
10
+
3
11
  ## 0.119.4
4
12
 
5
13
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.119.4",
3
+ "version": "0.119.5",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -25,6 +25,8 @@ export {
25
25
  getConfiguredEngineNameForRequest,
26
26
  getStoredModelForEngine,
27
27
  normalizeModelForEngine,
28
+ resolveEnginePreservesCustomModels,
29
+ type NormalizeModelOptions,
28
30
  detectEngineFromEnv,
29
31
  detectEngineFromEnvForRequest,
30
32
  detectEngineFromUserSecrets,
@@ -96,6 +96,42 @@ function packageNameFromInstallSpecifier(specifier: string): string | null {
96
96
  return versionIndex === -1 ? trimmed : trimmed.slice(0, versionIndex);
97
97
  }
98
98
 
99
+ /**
100
+ * True only when there is positive evidence this module is executing from a
101
+ * bundled serverless function where optional dependencies were inlined into the
102
+ * bundle and are therefore NOT resolvable via `require.resolve` — even though
103
+ * the dynamic `import()` the engine uses to load them still works.
104
+ *
105
+ * Deliberately narrow. The Nitro Vercel/Netlify presets (which agent-native's
106
+ * own `deploy` command emits) inline optional peers and always set these env
107
+ * markers, so they are a reliable signal. Other serverless runtimes — a
108
+ * container on Cloud Run / Google Cloud Functions (`K_SERVICE` /
109
+ * `FUNCTION_TARGET`), or a plain AWS Lambda — commonly ship a real
110
+ * `node_modules` where `require.resolve` is authoritative; there a resolve miss
111
+ * means the package is genuinely absent and must NOT be masked. Those runtimes
112
+ * are still covered *when the code is actually bundled*, via the module-path
113
+ * check below, which stays false for a normal `node_modules` layout.
114
+ */
115
+ function isBundledServerlessRuntime(): boolean {
116
+ const env = process.env;
117
+ // Nitro's Vercel/Netlify presets inline optional peers into the function
118
+ // bundle; these platforms always set these markers.
119
+ if (env.VERCEL || env.NETLIFY) return true;
120
+ // Otherwise require direct evidence that this module is running from inside a
121
+ // bundle output directory (Vercel's `/var/task`, Nitro's `.output/server`,
122
+ // inlined `_libs`). This is the real signal that `require.resolve` cannot be
123
+ // trusted; it stays false for normal `node_modules` layouts (dev, tests, and
124
+ // container/Lambda/Cloud Run deploys that ship their dependencies), so a
125
+ // genuine "package not installed" miss still surfaces there.
126
+ try {
127
+ return /[\\/](?:_libs|\.vercel|\.netlify|\.output)[\\/]|\/var\/task\//.test(
128
+ import.meta.url ?? "",
129
+ );
130
+ } catch {
131
+ return false;
132
+ }
133
+ }
134
+
99
135
  function canResolvePackage(packageName: string): boolean {
100
136
  const cached = _packageAvailabilityCache.get(packageName);
101
137
  if (cached !== undefined) return cached;
@@ -104,7 +140,15 @@ function canResolvePackage(packageName: string): boolean {
104
140
  require.resolve(packageName);
105
141
  available = true;
106
142
  } catch {
107
- available = false;
143
+ // Bundled serverless runtimes (e.g. Nitro on Vercel/Netlify) inline optional
144
+ // provider packages into the function bundle, so require.resolve cannot find
145
+ // them even though the dynamic `import()` the engine actually uses to load
146
+ // them works. Treat them as available there and let the engine's own import
147
+ // be the real gate — it already fails with a clear "pnpm add …" message when
148
+ // the package is genuinely missing. Without this, every engine-usability
149
+ // gate rejects the AI-SDK engines at runtime and the agent silently falls
150
+ // back to the native Anthropic engine.
151
+ available = isBundledServerlessRuntime();
108
152
  }
109
153
  _packageAvailabilityCache.set(packageName, available);
110
154
  return available;
@@ -179,17 +223,41 @@ function findLatestSupportedVersionMatch(
179
223
  return best?.model;
180
224
  }
181
225
 
226
+ export interface NormalizeModelOptions {
227
+ /**
228
+ * Force unrecognized (custom) model IDs to be kept verbatim, as if
229
+ * `engine.preserveCustomModels` were set on a live engine instance.
230
+ *
231
+ * The settings actions call `normalizeModelForEngine` with a static registry
232
+ * ENTRY, which never carries the runtime `preserveCustomModels` flag — that
233
+ * is only set on the engine INSTANCE created with an OpenAI-compatible
234
+ * `baseUrl`. They resolve the capability with
235
+ * {@link resolveEnginePreservesCustomModels} and pass it here so a gateway
236
+ * model (e.g. an Ollama `gemma4`) is not rewritten to the OpenAI default on
237
+ * save/read. First-party OpenAI (no gateway) leaves this unset, so an unknown
238
+ * or invalid model still normalizes to a supported one.
239
+ */
240
+ preserveCustomModels?: boolean;
241
+ }
242
+
182
243
  export function normalizeModelForEngine(
183
244
  engine: Pick<
184
245
  AgentEngine,
185
246
  "name" | "defaultModel" | "supportedModels" | "preserveCustomModels"
186
247
  >,
187
248
  model: string | null | undefined,
249
+ options: NormalizeModelOptions = {},
188
250
  ): string {
189
251
  const candidate = typeof model === "string" ? model.trim() : "";
190
252
  if (!candidate) return engine.defaultModel;
191
253
 
192
- if (engine.preserveCustomModels) return candidate;
254
+ // Preserve custom IDs verbatim BEFORE any catalog/version matching, so a
255
+ // version-shaped gateway model that happens to share a family with a
256
+ // built-in model (e.g. `gpt-5.4` on an OpenAI-compatible endpoint) is not
257
+ // rewritten to a catalog entry.
258
+ if (engine.preserveCustomModels || options.preserveCustomModels) {
259
+ return candidate;
260
+ }
193
261
 
194
262
  if (engine.supportedModels.length === 0) return candidate;
195
263
 
@@ -203,6 +271,31 @@ export function normalizeModelForEngine(
203
271
  );
204
272
  }
205
273
 
274
+ /**
275
+ * Whether models saved or read for this engine ENTRY should be preserved
276
+ * verbatim instead of normalized against the built-in catalog.
277
+ *
278
+ * `normalizeModelForEngine` honors a live engine's `preserveCustomModels`, but
279
+ * that flag is only set on an AI SDK engine INSTANCE when the OpenAI provider
280
+ * is pointed at an OpenAI-compatible gateway (a custom base URL — e.g. Ollama
281
+ * Cloud or LiteLLM), whose model IDs are not in the built-in OpenAI catalog.
282
+ * The static registry entry the settings actions pass to
283
+ * `normalizeModelForEngine` cannot carry that runtime flag, so this async
284
+ * helper reproduces the same decision — `ai-sdk:openai` AND a resolved base URL
285
+ * — from the request's stored/deploy config. First-party OpenAI (no gateway)
286
+ * returns false so an unknown/invalid model still normalizes to a supported one.
287
+ */
288
+ export async function resolveEnginePreservesCustomModels(
289
+ entry: Pick<AgentEngineEntry, "name">,
290
+ ): Promise<boolean> {
291
+ if (entry.name !== "ai-sdk:openai") return false;
292
+ try {
293
+ return Boolean(await resolveOpenAiBaseUrl());
294
+ } catch {
295
+ return false;
296
+ }
297
+ }
298
+
206
299
  function assertAgentEnginePackageInstalled(entry: AgentEngineEntry): void {
207
300
  if (isAgentEnginePackageInstalled(entry)) return;
208
301
  const installHint = entry.installPackage
@@ -13,6 +13,7 @@ import {
13
13
  isAgentEnginePackageInstalled,
14
14
  isStoredEngineUsableForRequest,
15
15
  normalizeModelForEngine,
16
+ resolveEnginePreservesCustomModels,
16
17
  } from "../../agent/engine/index.js";
17
18
  import type { ActionTool } from "../../agent/types.js";
18
19
  import { getSetting } from "../../settings/index.js";
@@ -82,11 +83,18 @@ export async function run(args: Record<string, string> = {}): Promise<string> {
82
83
  ? current?.model
83
84
  : undefined;
84
85
  const currentEngineName = currentEntry?.name ?? "anthropic";
86
+ // Resolve the OpenAI-compatible-endpoint capability so a custom gateway model
87
+ // is reported as-is instead of being normalized to the engine default — the
88
+ // read-side counterpart of the same fix in set-/manage-agent-engine.
89
+ const preserveCustomModels = currentEntry
90
+ ? await resolveEnginePreservesCustomModels(currentEntry)
91
+ : false;
85
92
  const currentModel =
86
93
  currentEntry && !envUnavailable
87
94
  ? normalizeModelForEngine(
88
95
  currentEntry,
89
96
  currentModelCandidate ?? currentEntry.defaultModel,
97
+ { preserveCustomModels },
90
98
  )
91
99
  : (currentModelCandidate ?? DEFAULT_MODEL);
92
100
  const result = {
@@ -16,6 +16,7 @@ import {
16
16
  getAgentEngineEntry,
17
17
  isAgentEnginePackageInstalled,
18
18
  normalizeModelForEngine,
19
+ resolveEnginePreservesCustomModels,
19
20
  registerBuiltinEngines,
20
21
  } from "../../agent/engine/index.js";
21
22
  import type { ActionTool } from "../../agent/types.js";
@@ -113,7 +114,10 @@ async function runSetAppDefault(args: Record<string, string>): Promise<string> {
113
114
  if (!isAgentEnginePackageInstalled(entry)) {
114
115
  return `Error: Engine "${engine}" requires optional packages that are not installed in this app. Run: pnpm add ${entry.installPackage}`;
115
116
  }
116
- const normalizedModel = normalizeModelForEngine(entry, model);
117
+ const preserveCustomModels = await resolveEnginePreservesCustomModels(entry);
118
+ const normalizedModel = normalizeModelForEngine(entry, model, {
119
+ preserveCustomModels,
120
+ });
117
121
 
118
122
  const ctx = currentContext();
119
123
  const canUpdate = await canUpdateAgentAppModelDefaultSettings(
@@ -8,6 +8,7 @@ import {
8
8
  isAgentEnginePackageInstalled,
9
9
  isStoredEngineUsableForRequest,
10
10
  normalizeModelForEngine,
11
+ resolveEnginePreservesCustomModels,
11
12
  registerBuiltinEngines,
12
13
  } from "../../agent/engine/index.js";
13
14
  import type { ActionTool } from "../../agent/types.js";
@@ -54,7 +55,14 @@ export async function run(args: Record<string, string>): Promise<string> {
54
55
  }
55
56
 
56
57
  const requestedModel = model ?? entry.defaultModel;
57
- const resolvedModel = normalizeModelForEngine(entry, requestedModel);
58
+ // A static registry entry can't carry the runtime `preserveCustomModels`
59
+ // flag, so resolve the OpenAI-compatible-endpoint capability here and pass it
60
+ // through — otherwise a gateway model (e.g. an Ollama id) is rewritten to the
61
+ // engine default on save.
62
+ const preserveCustomModels = await resolveEnginePreservesCustomModels(entry);
63
+ const resolvedModel = normalizeModelForEngine(entry, requestedModel, {
64
+ preserveCustomModels,
65
+ });
58
66
 
59
67
  const usable = await isStoredEngineUsableForRequest(
60
68
  { engine: engineName },
@@ -26,6 +26,7 @@ import { z } from "zod";
26
26
  import { normalizeSlidePadding } from "../app/lib/normalize-slide-padding.js";
27
27
  import { getDb, schema } from "../server/db/index.js";
28
28
  import { notifyClients } from "../server/handlers/decks.js";
29
+ import { ASPECT_RATIO_VALUES } from "../shared/aspect-ratios.js";
29
30
 
30
31
  // ---------------------------------------------------------------------------
31
32
  // Per-deck write lock — same pattern as add-slide.ts so all client and agent
@@ -122,7 +123,7 @@ const PatchDeckFieldsOp = z.object({
122
123
  tweaks: z
123
124
  .record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
124
125
  .optional(),
125
- aspectRatio: z.string().optional(),
126
+ aspectRatio: z.enum(ASPECT_RATIO_VALUES).optional(),
126
127
  shareToken: z.string().optional(),
127
128
  visibility: z.enum(["private", "org", "public"]).optional(),
128
129
  })
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-23
4
+ ---
5
+
6
+ Opening or refining a deck no longer crashes with a blank error screen when the deck has an unrecognized aspect ratio
@@ -30,5 +30,8 @@ export const ASPECT_RATIO_VALUES = ["16:9", "1:1", "9:16", "4:5"] as const;
30
30
  export const DEFAULT_ASPECT_RATIO: AspectRatio = "16:9";
31
31
 
32
32
  export function getAspectRatioDims(ratio: AspectRatio | undefined | null) {
33
- return ASPECT_RATIOS[ratio ?? DEFAULT_ASPECT_RATIO];
33
+ if (ratio && Object.prototype.hasOwnProperty.call(ASPECT_RATIOS, ratio)) {
34
+ return ASPECT_RATIOS[ratio];
35
+ }
36
+ return ASPECT_RATIOS[DEFAULT_ASPECT_RATIO];
34
37
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tabWidth": 2,
3
+ "useTabs": false,
4
+ "printWidth": 80,
5
+ "trailingComma": "all",
6
+ "proseWrap": "preserve",
7
+ "sortImports": true
8
+ }
@@ -7,6 +7,11 @@ inventory existing public kits and installed package seams. Use
7
7
  `customizing-agent-native` for the configure → compose → eject → propose seam
8
8
  ladder.
9
9
 
10
+ ## Authoritative References
11
+
12
+ - [Feature docs](./docs/features/README.md) define product behavior and feature status.
13
+ - [Action definitions](./actions/) contain the canonical tool descriptions, input schemas, and implementations.
14
+
10
15
  ## Core Rules
11
16
 
12
17
  - Never hardcode API keys, tokens, webhook URLs, signing secrets, private Builder/internal data, customer data, or credential-looking literals. Use secrets/OAuth/runtime configuration and obvious placeholders in examples.
@@ -16,34 +21,38 @@ ladder.
16
21
  - Keep the action surface small: task CRUD actions plus `reorder-tasks`, `view-screen`, and `navigate`.
17
22
  - Do not use `db-query` for normal task operations.
18
23
  - Call `view-screen` first when the user's visible task context matters (especially on `/tasks`).
24
+ - Tasks are private to each user. Preserve `ownerEmail` scoping unless intentionally implementing sharing.
25
+ - The task detail panel exposes `tasks.task-detail.bottom` as an `ExtensionSlot` with `slotContext` containing `taskId`, `title`, `done`, and `fieldValues`.
19
26
 
20
27
  ## Actions
21
28
 
22
- | Action | Method | Purpose |
23
- | ---------------------------- | ------ | -------------------------------------------------------------------------------------------- |
24
- | `list-tasks` | GET | List current user's tasks; `includeDone` and `includeFields` default to false |
25
- | `create-task` | POST | Create a task with `title` |
26
- | `update-task` | POST | Patch `title`, `done`, and/or `fieldValues` by `taskId` |
27
- | `delete-task` | POST | Delete a task by `taskId` (confirm with user first) |
28
- | `bulk-update-tasks` | POST | Patch `title` and/or `done` on multiple tasks by id |
29
- | `bulk-delete-tasks` | POST | Delete multiple tasks by id (confirm with user first) |
30
- | `reorder-tasks` | POST | Reorder visible tasks by id list top-to-bottom |
31
- | `list-inbox-items` | GET | List current user's inbox items |
32
- | `create-inbox-item` | POST | Create a not-ready inbox item with `title` (default chat capture) |
33
- | `update-inbox-item` | POST | Patch inbox item `title` by `inboxItemId` |
34
- | `delete-inbox-item` | POST | Delete an inbox item (confirm with user first) |
35
- | `mark-inbox-item-ready` | POST | Promote inbox item to an incomplete task |
36
- | `reorder-inbox-items` | POST | Reorder inbox items by id list top-to-bottom |
37
- | `list-custom-fields` | GET | List custom field definitions |
38
- | `create-custom-field` | POST | Create a custom field definition with `title`, `type`, and optional `config` |
39
- | `update-custom-field` | POST | Patch a field definition `title` and/or type-compatible `config`; type is immutable |
40
- | `delete-custom-field` | POST | Delete a field definition and its values on every task (confirm with user first) |
41
- | `reorder-custom-fields` | POST | Reorder custom field definitions by id list top-to-bottom |
42
- | `list-visible-task-fields` | GET | List custom field ids shown on task cards for the current user |
43
- | `update-visible-task-fields` | POST | Replace which custom fields appear on task cards (max 3) |
44
- | `view-screen` | | Read navigation, UI bulk selection, visible tasks, and inbox snapshot |
45
- | `navigate` | | Move UI to a view: `tasks`, `inbox`, `fields`, `extensions`, `team` (`home`/`ask` → `tasks`) |
46
- | `render-task-list-inline` | — | Render an interactive task-list widget inline in chat without leaving the current view |
29
+ | Action | Method | Purpose |
30
+ | ----------------------------- | ------ | -------------------------------------------------------------------------------------------- |
31
+ | `list-tasks` | GET | List current user's tasks; `includeDone` and `includeFields` default to false |
32
+ | `create-task` | POST | Create a task with `title` |
33
+ | `update-task` | POST | Patch `title`, `done`, and/or `fieldValues` by `taskId` |
34
+ | `delete-task` | POST | Delete a task by `taskId` (confirm with user first) |
35
+ | `bulk-update-tasks` | POST | Patch `title` and/or `done` on multiple tasks by id |
36
+ | `bulk-delete-tasks` | POST | Delete multiple tasks by id (confirm with user first) |
37
+ | `reorder-tasks` | POST | Reorder visible tasks by id list top-to-bottom |
38
+ | `list-inbox-items` | GET | List current user's inbox items |
39
+ | `create-inbox-item` | POST | Create a not-ready inbox item with `title` (default chat capture) |
40
+ | `update-inbox-item` | POST | Patch inbox item `title` by `inboxItemId` |
41
+ | `delete-inbox-item` | POST | Delete an inbox item (confirm with user first) |
42
+ | `bulk-delete-inbox-items` | POST | Delete multiple inbox items by id (confirm with user first) |
43
+ | `mark-inbox-item-ready` | POST | Promote inbox item to an incomplete task |
44
+ | `bulk-mark-inbox-items-ready` | POST | Promote multiple inbox items to incomplete tasks by id |
45
+ | `reorder-inbox-items` | POST | Reorder inbox items by id list top-to-bottom |
46
+ | `list-custom-fields` | GET | List custom field definitions |
47
+ | `create-custom-field` | POST | Create a custom field definition with `title`, `type`, and optional `config` |
48
+ | `update-custom-field` | POST | Patch a field definition `title` and/or type-compatible `config`; type is immutable |
49
+ | `delete-custom-field` | POST | Delete a field definition and its values on every task (confirm with user first) |
50
+ | `reorder-custom-fields` | POST | Reorder custom field definitions by id list top-to-bottom |
51
+ | `list-visible-task-fields` | GET | List custom field ids shown on task cards for the current user |
52
+ | `update-visible-task-fields` | POST | Replace which custom fields appear on task cards (max 3) |
53
+ | `view-screen` | — | Read navigation, UI bulk selection, visible tasks, and inbox snapshot |
54
+ | `navigate` | — | Move UI to a view: `tasks`, `inbox`, `fields`, `extensions`, `team` (`home`/`ask` → `tasks`) |
55
+ | `render-task-list-inline` | — | Render an interactive task-list widget inline in chat without leaving the current view |
47
56
 
48
57
  ## Store Functions And Transactions
49
58
 
@@ -80,22 +89,6 @@ export type DbHandle = Pick<
80
89
 
81
90
  ## Commit Message Conventions
82
91
 
83
- - Never include `Made-with: Cursor` in commit messages. Remove it if it appears
84
- in a generated message.
85
- - Use one of these prefixes:
86
- - `feature: ...` or `feature(PROJECT): ...`
87
- - `fix: ...` or `fix(PROJECT): ...`
88
- - `refactor: ...` or `refactor(PROJECT): ...`
89
- - `technical: ...` or `technical(PROJECT): ...`
90
- - `chore: ...` or `chore(PROJECT): ...`
91
- - `PROJECT` is optional. If provided, it must be one of `generator` or `web`.
92
- - Before creating any commit, always:
93
- - ask for confirmation,
94
- - show the proposed commit message first,
95
- - commit only after explicit user approval.
96
-
97
- ## Commit Message Conventions
98
-
99
92
  - Never include `Made-with: Cursor` in commit messages. Remove it if it appears
100
93
  in a generated message.
101
94
  - Use one of these prefixes:
@@ -4,13 +4,20 @@ Task-list-first agent-native app. Default home is `/tasks`; chat handles capture
4
4
 
5
5
  ## TODO
6
6
 
7
- - [ ] **i18n** — Add `app/i18n/` catalogs (`en-US` + locale mirrors) and migrate UI copy from raw literals to `useT()`. Remove tasks strings from `scripts/i18n-raw-literal-baseline.txt` once migrated.
8
- - [ ] **Delete dialog wiring** — `TaskList` and `InboxList` duplicate single/bulk delete dialog state (`pendingDeleteId`, `bulkDeleteOpen`, confirm handlers, toast on bulk success). Extract a shared hook or composer helper (e.g. `useDeleteDialogs`) that owns pending id, open state, and confirm wiring while composers pass entity labels and delete mutations.
9
- - [ ] Extract shared action schema helpers (e.g. `taskId` field, title patch) once the action surface grows beyond MVP.
10
- - [ ] Add route-level `ErrorBoundary` and `Suspense` around task list loading (align with other agent-native templates).
7
+ - [ ] **Runtime action discovery filtering** — Update `@agent-native/core` development action discovery to exclude colocated `*.test.*`, `*.spec.*`, and helper-only modules so they are never exposed as agent tools or HTTP action routes.
8
+ - [ ] **Optional event-triggered automations** — If Tasks needs workflows such as “when a task is completed, notify another system,” register stable domain events in `server/plugins/agent-chat.ts` and emit owner-scoped payloads from the successful write paths. Add event discovery, payload-validation, transition, and failure-isolation tests before exposing the events as a public automation contract.
11
9
  - [ ] **`view-screen` agent snapshot** — Revisit `AGENT_TASKS_LIST_CAP` in `actions/view-screen.ts`: it caps the agent tool payload (token budget), not the UI list. Confirm whether `list` / `selectedItem` / `inListSnapshot` correctly represent what the user sees, tune or document the cap, and align with UI limits if needed. Also review Zod parsing of `navigation` / app-state selection keys for strictness vs backward compatibility.
12
- - [ ] **Selection context** — Revisit prop drilling through `TaskList` → header/bar/rows once the composer stabilizes; a narrow context for `{ state, actions }` would trim wiring but must not blur the `dnd/` ↔ `selection/` boundary.
13
10
  - [ ] **`TaskList` optimistic state** — Refactor manual `allTasks` buffer in `app/components/tasks/TaskList.tsx` to TanStack Query `onMutate` once exit animations and reorder rollback can share one cache strategy.
14
11
  - [ ] **Exit-animation list reconcile** — Move exit-animation reconcile / `exitingIdsRef` out of `app/components/tasks/TaskList.tsx` into a neutral list helper (e.g. `use-ordered-tasks-with-exit-animation.ts`). That logic keeps completed rows visible during CSS exit when Show all is off; it is not DnD. `TaskList` should pass an ordered list into generic `SortableList`.
15
- - Shared `ListRowPreview` (`app/components/shared/dnd/ListRowPreview.tsx`) still duplicates row chrome; render the live row in the overlay when `SortableList` can mount previews safely (see TODO in that file).
16
12
  - [ ] **`move-tasks` / relative reorder (try on a separate branch)** — Today DnD persistence is FE-heavy: `SortableList` runs `reorderMovingItems` (`app/components/dnd/reorder-moving-items.ts`) to compute the **full visible id list**, then `TaskList` calls `reorder-tasks` with every id top-to-bottom. The BE already persists order via `server/tasks/store.ts` `reorderTasks` — no separate `bulkReorder` needed, but the **drop → order** translation is brittle (multi-select before/after/top/bottom edge cases). **Proposal:** add a relative move action, e.g. `move-tasks` with `{ taskIds, beforeId? | afterId?, includeDone? }` (or extend `reorder-tasks` with a relative mode). BE applies the block move, validates visible list / ownership, rewrites `sortOrder`, returns updated tasks. Keep **absolute** full-list `taskIds[]` as an escape hatch for agent/import flows. **FE after refactor:** dnd-kit still handles overlay/collision/optimistic UI, but on drop only derives `{ selected ids, anchor id, before | after }` from `over` + drag direction — delete most of `computeBlockInsertIndex`. **Prefer `beforeId`/`afterId` over `toIndex`** — matches user/agent language (“put these after task Z”) and avoids index ambiguity with `includeDone` / hidden rows. **Touch:** new `actions/move-tasks.ts`, `server/tasks/store.ts`, `use-tasks.ts`, `TaskList.tsx` / `InboxList.tsx`, agent prompt in `server/plugins/agent-chat.ts`, unit tests on store + thin FE mapping tests; keep `reorder-tasks` until callers migrate.
13
+ - [ ] **List pagination + virtualization (when volume warrants)** — `listStoredItems` is unbounded and rows render unvirtualized. Fine at personal-list volumes; if lists grow, adopt mail's pattern (`useInfiniteQuery` with page size 25 + `@tanstack/react-virtual` in `EmailList.tsx`). Parked by the 2026-07-17 sibling-parity review as a latent scaling concern, not an active bug.
14
+
15
+ ## Optional improvements
16
+
17
+ Reviewed against sibling templates (starter, mail, content, calendar, plan): none of these has an established cross-template convention to adopt, so they are optional polish rather than alignment work.
18
+
19
+ - [ ] **Delete dialog wiring** — `TaskList` and `InboxList` duplicate single/bulk delete dialog state (`pendingDeleteId`, `bulkDeleteOpen`, confirm handlers, toast on bulk success); a shared `useDeleteDialogs` hook could own that wiring. Siblings: no template has a shared delete-confirm hook — each inlines shadcn `AlertDialog` per call site (calendar's `DeleteEventDialog` is feature-specific). tasks' shared `DeleteItemDialog`/`BulkDeleteDialog` components already lead here; this would be a net-new pattern, not adoption.
20
+ - [ ] **Shared action schema helpers** — Extract shared helpers (e.g. `taskId` field, title patch) if duplication grows. Siblings: no template has generic schema helpers — every action inlines `z.object({...})` in `defineAction`; the only shared primitives are feature-specific (clips' `booleanParam`, tasks' own `boolean-query-param`). Wait for a concrete third duplication before abstracting.
21
+ - [ ] **Custom `ErrorBoundary` copy** — tasks already exports the root-level `ErrorBoundary` (re-export from `@agent-native/core/client`), which is full boundary parity: no template has per-route boundaries, and `Suspense` only wraps `lazy()` components — data loads through react-query, not loaders. The only optional delta: mail and content (2 of 4) replace the re-export with custom copy plus `ErrorReportActions` (send feedback / open issue).
22
+ - [ ] **Task attachments** — no `@agent-native/core/file-upload` usage anywhere, though `.gitignore` already carries the standard `data/uploads/` entry. mail/content/plan use the framework file-upload module. A product decision, not an alignment gap.
23
+ - [ ] **Selection context** — Revisit prop drilling through `TaskList` → header/bar/rows; a narrow `{ state, actions }` context would trim wiring but must not blur the `dnd/` ↔ `selection/` boundary. Siblings: no template uses a React context for list multi-select — the universal pattern is a `useState<Set<string>>` hook plus prop drilling (mail's `EmailList` passes `selectedIds` down as props), and tasks' `use-list-selection.ts` is already the most generalized version of it.
@@ -4,16 +4,18 @@ import { z } from "zod";
4
4
  import { deleteInboxItems, requireUserEmail } from "../server/inbox/store.js";
5
5
  import { BULK_ID_LIMIT } from "../shared/bulk-limits.js";
6
6
 
7
+ export const bulkDeleteInboxItemsSchema = z.object({
8
+ inboxItemIds: z
9
+ .array(z.string())
10
+ .min(1)
11
+ .max(BULK_ID_LIMIT)
12
+ .describe("Inbox item ids to delete"),
13
+ });
14
+
7
15
  export default defineAction({
8
16
  description:
9
- "Delete multiple inbox items permanently. Ask the user to confirm before calling.",
10
- schema: z.object({
11
- inboxItemIds: z
12
- .array(z.string())
13
- .min(1)
14
- .max(BULK_ID_LIMIT)
15
- .describe("Inbox item ids to delete"),
16
- }),
17
+ "Delete multiple inbox items permanently. Use delete-inbox-item for one item. Ask the user to confirm before calling.",
18
+ schema: bulkDeleteInboxItemsSchema,
17
19
  run: async (args, ctx) => {
18
20
  const ownerEmail = requireUserEmail(ctx?.userEmail);
19
21
  return deleteInboxItems({
@@ -4,16 +4,18 @@ import { z } from "zod";
4
4
  import { deleteTasks, requireUserEmail } from "../server/tasks/store.js";
5
5
  import { BULK_ID_LIMIT } from "../shared/bulk-limits.js";
6
6
 
7
+ export const bulkDeleteTasksSchema = z.object({
8
+ taskIds: z
9
+ .array(z.string())
10
+ .min(1)
11
+ .max(BULK_ID_LIMIT)
12
+ .describe("Task ids to delete"),
13
+ });
14
+
7
15
  export default defineAction({
8
16
  description:
9
- "Delete multiple tasks permanently. Ask the user to confirm before calling.",
10
- schema: z.object({
11
- taskIds: z
12
- .array(z.string())
13
- .min(1)
14
- .max(BULK_ID_LIMIT)
15
- .describe("Task ids to delete"),
16
- }),
17
+ "Delete multiple tasks permanently. Use delete-task for one task. Ask the user to confirm before calling.",
18
+ schema: bulkDeleteTasksSchema,
17
19
  run: async (args, ctx) => {
18
20
  const ownerEmail = requireUserEmail(ctx?.userEmail);
19
21
  return deleteTasks({ ownerEmail, ids: args.taskIds });
@@ -9,7 +9,7 @@ import { BULK_ID_LIMIT } from "../shared/bulk-limits.js";
9
9
 
10
10
  export default defineAction({
11
11
  description:
12
- "Promote multiple inbox items to incomplete tasks in one atomic batch.",
12
+ "Promote multiple inbox items to incomplete tasks in one atomic batch, preserving their ids. Use mark-inbox-item-ready for one item.",
13
13
  schema: z.object({
14
14
  inboxItemIds: z
15
15
  .array(z.string())
@@ -5,18 +5,20 @@ import { UserInputError } from "../server/errors.js";
5
5
  import { requireUserEmail, updateTasks } from "../server/tasks/store.js";
6
6
  import { BULK_ID_LIMIT } from "../shared/bulk-limits.js";
7
7
 
8
+ export const bulkUpdateTasksSchema = z.object({
9
+ taskIds: z
10
+ .array(z.string())
11
+ .min(1)
12
+ .max(BULK_ID_LIMIT)
13
+ .describe("Task ids to update"),
14
+ title: z.string().min(1).optional().describe("New title for every task"),
15
+ done: z.boolean().optional().describe("Completion state for every task"),
16
+ });
17
+
8
18
  export default defineAction({
9
19
  description:
10
- "Update multiple tasks with the same title and/or completion patch.",
11
- schema: z.object({
12
- taskIds: z
13
- .array(z.string())
14
- .min(1)
15
- .max(BULK_ID_LIMIT)
16
- .describe("Task ids to update"),
17
- title: z.string().min(1).optional().describe("New title for every task"),
18
- done: z.boolean().optional().describe("Completion state for every task"),
19
- }),
20
+ "Apply the same title and/or completion state to multiple tasks. Use update-task for one task or custom field values.",
21
+ schema: bulkUpdateTasksSchema,
20
22
  run: async (args, ctx) => {
21
23
  const ownerEmail = requireUserEmail(ctx?.userEmail);
22
24
  if (args.title === undefined && args.done === undefined) {
@@ -5,11 +5,14 @@ import { z } from "zod";
5
5
  import { createInboxItem, requireUserEmail } from "../server/inbox/store.js";
6
6
  import type { InboxItem } from "../server/inbox/store.js";
7
7
 
8
+ export const createInboxItemSchema = z.object({
9
+ title: z.string().min(1).describe("Inbox item title"),
10
+ });
11
+
8
12
  export default defineAction({
9
- description: "Create a not-ready inbox item with a title.",
10
- schema: z.object({
11
- title: z.string().min(1).describe("Inbox item title"),
12
- }),
13
+ description:
14
+ "Create a not-ready inbox item for later triage. Use create-task when the user explicitly wants it added directly to the task list.",
15
+ schema: createInboxItemSchema,
13
16
  run: async (args, ctx) => {
14
17
  const ownerEmail = requireUserEmail(ctx?.userEmail);
15
18
  const item = await createInboxItem({ ownerEmail, title: args.title });
@@ -9,7 +9,8 @@ import {
9
9
  } from "../server/tasks/store.js";
10
10
 
11
11
  export default defineAction({
12
- description: "Create a new task with a title.",
12
+ description:
13
+ "Create an incomplete task directly on the task list. Use create-inbox-item for a quick capture to triage later.",
13
14
  schema: z.object({
14
15
  title: z.string().min(1).describe("Task title"),
15
16
  }),
@@ -3,12 +3,14 @@ import { z } from "zod";
3
3
 
4
4
  import { deleteInboxItem, requireUserEmail } from "../server/inbox/store.js";
5
5
 
6
+ export const deleteInboxItemSchema = z.object({
7
+ inboxItemId: z.string().describe("Inbox item id"),
8
+ });
9
+
6
10
  export default defineAction({
7
11
  description:
8
- "Delete an inbox item permanently. Ask the user to confirm before calling.",
9
- schema: z.object({
10
- inboxItemId: z.string().describe("Inbox item id"),
11
- }),
12
+ "Delete one inbox item permanently. Use bulk-delete-inbox-items for multiple items. Ask the user to confirm before calling.",
13
+ schema: deleteInboxItemSchema,
12
14
  run: async (args, ctx) => {
13
15
  const ownerEmail = requireUserEmail(ctx?.userEmail);
14
16
  await deleteInboxItem({ ownerEmail, id: args.inboxItemId });
@@ -5,7 +5,7 @@ import { deleteTask, requireUserEmail } from "../server/tasks/store.js";
5
5
 
6
6
  export default defineAction({
7
7
  description:
8
- "Delete a task permanently. Ask the user to confirm before calling.",
8
+ "Delete one task permanently. Use bulk-delete-tasks for multiple tasks. Ask the user to confirm before calling.",
9
9
  schema: z.object({
10
10
  taskId: z.string().describe("Task id"),
11
11
  }),
@@ -3,9 +3,11 @@ import { z } from "zod";
3
3
 
4
4
  import { listInboxItems, requireUserEmail } from "../server/inbox/store.js";
5
5
 
6
+ export const listInboxItemsSchema = z.object({});
7
+
6
8
  export default defineAction({
7
- description: "List inbox items for the current user (not-ready capture).",
8
- schema: z.object({}),
9
+ description: "List not-ready inbox items awaiting triage.",
10
+ schema: listInboxItemsSchema,
9
11
  http: { method: "GET" },
10
12
  readOnly: true,
11
13
  run: async (_args, ctx) => {
@@ -4,10 +4,12 @@ import { z } from "zod";
4
4
  import { requireUserEmail } from "../server/custom-fields/store.js";
5
5
  import { getTaskCardFieldIds } from "../server/user-config/store.js";
6
6
 
7
+ export const listVisibleTaskFieldsSchema = z.object({});
8
+
7
9
  export default defineAction({
8
10
  description:
9
11
  "List custom field ids currently shown on task cards for the current user.",
10
- schema: z.object({}),
12
+ schema: listVisibleTaskFieldsSchema,
11
13
  http: { method: "GET" },
12
14
  readOnly: true,
13
15
  run: async (_args, ctx) => {