* feat(ui-library): add the approval-ui block
Presentational components for an approval process: progress, timeline,
route preview, parallel branches, receipts and an action bar with forms.
They take data already shaped and translated for display and depend on
no plugin. Includes a preview demo, its registry example and tests.
* docs(app-skills): list the approval-ui block in the UI Library catalog
* feat(agents): limit runs per coding tool on a runner
Co-authored-by: Project lead <agent+studio-project-lead@studio.noreply>
* feat(agent-runner): limit runs per coding tool with --slots
Co-authored-by: Project lead <agent+studio-project-lead@studio.noreply>
* feat(agents): edit and show limits per coding tool on the runtimes page
Co-authored-by: Project lead <agent+studio-project-lead@studio.noreply>
* chore: format and add the changeset for limits per coding tool
Co-authored-by: Project lead <agent+studio-project-lead@studio.noreply>
* fix(agents): make the fields for limits per coding tool optional in shared types
* fix(agents): align queue component with per-tool wait reason
* feat(agents): one tools table per runtime, saved with its settings
---------
Co-authored-by: Project lead <agent+studio-project-lead@studio.noreply>
* feat(app-plugin-agents): send custom request headers and a per-conversation session id to model services
A model service can carry request headers of its own (a value may be secret: sealed like the API key and never answered) and name a session header, such as OpenCode Go's x-opencode-session, under which every model call of a conversation sends one stable id. Every request to a provider names the plugin first in its user agent.
Co-authored-by: Project lead <agent+studio-project-lead@studio.noreply>
* fix(app-plugin-agents): send OpenCode's session id without a service setting
A model service no longer names a session header. A call to an OpenCode base URL (Zen or Go) carries x-opencode-session by itself, with one session id per conversation and a new one for each call outside any; the id hashes the conversation's subject and thread. The service form drops the session-header field.
* refactor(app-plugin-agents): keep only the OpenCode session header
Plain services no longer send request headers of their own: the headers and headersEncrypted columns, the service API's headers field, the form's header table and the sealing they needed are removed. A call to an OpenCode base URL (Zen or Go) carries x-opencode-session automatically, with one session id per conversation and a new one for each call outside any, and every request names the plugin first in its user agent.
---------
Co-authored-by: Project lead <agent+studio-project-lead@studio.noreply>
Add record lifecycle definitions, transactional transitions, durable effects, recovery, jobs integration, and React bindings. Include ticket, expense, and office flow examples in the Examples application, with concurrency and database compatibility fixes.
Group generated plugin tests by source directory, provide shared Node test configuration, and document the layout. Include TSX tests in Node projects with a regression test for test discovery.
The daily trend chart fixed its Y axis at 56px, which fits token counts but clips cost ticks such as US$20.00 at the chart's left edge. Let recharts measure the formatted labels instead.
Co-authored-by: Project lead <agent+studio-project-lead@studio.noreply>
* fix(projects): keep an invitation's row menu open when the list renders again
The columns depended on the whole useMutation result, a new object each render, so every cell remounted.
* test: open popups the way a user does
A Base UI trigger is wired to its popup only once React runs the passive effects of the render that mounted it, so a bare fireEvent.click right after the trigger appears is dropped under load. Open popups with userEvent, fix the timing races this exposed, and cancel pending replies when the UI Library chat demo resets.
* chore: add a changeset for the new test dependency
* Add the agents, projects, knowledge and releases plugins
Four plugins for agent-run applications, the UI Library's agents and projects items, and the generated UI Library catalog of the application Skill with its check.
* Wait on the sent attachment's link as it renders last in the chat test
* Keep the demo chat's reply timers off window, so they outlive a test's environment quietly
* Update the plugins, templates and UI Library for the new runtime
Device authorization, scoped API keys, service accounts, user invitations and preferences, secrets-backed credentials, the inbox and the cn package across the existing plugins, examples and the three templates, with the UI Library's auth, inbox, authorization and component items.
* Declare the UI Library site's favicon
* Use the NocoBase favicon on the UI Library site
* Fix generated-app shell tests and the Hub's user deletion test
Inline the authentication client in the templates' Vitest config so its guards share the test's react-router, and update the Hub test to the users plugin's new copy and spinner label.
Encrypted secrets, a command manifest derived from the API document, environment-variable manifests, sample data and on-demand Apps in the runtime, plus the new agent protocol, application CLI client, agent runner and Docker App Host packages.
* fix(app-plugin-ai-employee): translate the nocobase-ai Registry item in the plugin namespace
The Registry item translated through its own locale files and a useAITranslate()
hook that picked a language by prefix and fell back to the English default, so a
key it never defined showed English in every language without anything noticing.
It now calls useTranslation('@nocobase/app-plugin-ai-employee') like the file
plugin's Registry item and the plugin's own pages, and its copy moves into the
plugin's client/locales, which the registered plugin loads.
Four keys the item used without defining them get English and Chinese text, and
a test fails when a key the Registry passes to t() is missing from either locale
file or a file translates outside the plugin namespace.
* feat(examples): add an AI employee tasks example and a global AI employee entry
@nocobase/app-plugin-ai-employee-example registers the AI employee iris and the
read-only tool example-ticket-history through AIResourceRegistrar, declares the
/ai-employee-example route with a fallback page, and publishes a tasks-page
Registry item that replaces it: support tickets whose AIEmployeeShortcut offers
an auto-sent analysis and a reply left in the composer, with the selected ticket
as work context, and a button that starts a queue triage through the global chat
controller.
The examples template installs the nocobase-ai item, wraps its signed-in layout
in a global AI employee entry (a floating trigger that opens the shared
conversation as a side panel and expands into a dialog), registers the example
plugin, installs its tasks page and links it from the homepage.
* test(templates): let the Examples layout carry its AI employee entry
The framework alignment check compares the Examples layout with Default byte for
byte, and the global AI employee entry wraps the signed-in shell. Like the
notification button in the header, it is an Examples-owned demonstration: the
check now removes exactly that wrapper, its import and its marker comment,
re-indents what it wraps, and still requires the shell itself to match Default.
* feat(app-plugin-ai-employee): release unused conversation checkpoints on a schedule
A recurring job on the application's jobs service releases the agent
checkpoints of conversations nobody has used for a week, every day at
03:00 UTC by default. Releasing a conversation sets its thread to 0 and
deletes the checkpoints of every thread it has had, in one transaction per
batch, only while the conversation is still unused. A conversation whose
latest message asks for a tool call is never released.
The next run on a released conversation replays its latest stored messages
onto a fresh thread. This applies only to conversations kept in the
plugin's own tables, never to a resume or a tool decision, and never to a
thread-0 conversation that still has a checkpoint there or has no messages,
so existing runs behave as before.
The schedule is configured under ai.checkpointCleanup and validated by
config check.
* feat(app-plugin-ai-employee): default a conversation's thread to 1
A conversation starts on thread 1 when AIConversationsManager creates it,
but the aiConversations.thread column defaulted to 0, so a row inserted
without a thread looked like a conversation whose checkpoints the cleanup
had released. A new migration sets the default to 1.
Thread 0 now always means a released conversation, so the agent decides
whether to replay stored messages from the thread number alone, without
reading the checkpoint store.
* fix(app-plugin-ai-employee): read each cleanup batch's latest messages in two queries
The checkpoint cleaner looked up the latest message of every candidate
conversation with its own query. It now finds each conversation's latest
message id with one groupBy over the batch and reads those messages with one
more query.
The aggregate goes through the Repository rather than the query builder,
because it reads a bigint result back exactly on every dialect, and a
message id is a snowflake beyond the integers a JavaScript number holds.
* refactor(templates): move Playwright tests to tests/playwright/ and drop the local AI e2e test
Keep every test under tests/: Playwright's testDir is tests/playwright/,
Vitest excludes that directory, and tsconfig.node.json typechecks it. The
directory ships with the template like the rest of tests/. The optional
local AI server test is removed; test:e2e still passes with no tests.
The application Skills describe the new location, and the upgrade Skill
says how an existing application moves its e2e/ files.
* refactor(app-template-hub): exclude tests/playwright/ from Vitest like the other templates
The template alignment test requires the three templates to share one
vitest.config.ts. Hub has no Playwright, so the exclusion changes nothing
there today, and a Hub application that adds Playwright already has it.
Replace the single `page:ai.settings` grant with seven settings items under
System management → AI, one per settings page: `ai.employees`,
`ai.llmServices` and `ai.mcpServers` with `read` and `manage`, and
`ai.skills`, `ai.tools`, `ai.usage` and `ai.conversations` with `read`.
Every management route names the permissions it accepts, pages open with
`read` and turn their controls off without `manage`, and previewing another
user's AI file requires `read` on `ai.conversations`.
A migration gives every item and action to each Permission Set that held the
page grant. A grant of every page no longer opens the AI settings.
Remove the management routes no settings page called: employee templates,
create and delete, skill and tool writes, single LLM service and MCP server
reads, and both MCP connection tests. The package moves to the 3.0.0
prerelease line.
* fix(file): time out Office Open XML previews that never finish loading
* fix(file): extend the Office Open XML preview timeout to 3 minutes
Let the first failure settle the preview inside reportViewerError, so it
no longer reads the timeout handle before it is declared and the timeout
callback no longer depends on statement order.
* fix(app-plugin-ai-employee): save a refused message with its stored columns only
A send refused at the parallel conversation limit saved the user message by
spreading it into the insert, so the key the chat sends with it reached
ai_messages, which has no such column, and the request failed with a SQL
error instead of the limit message.
* fix(app-plugin-ai-employee): count a run towards the limit from when it started
The parallel run limit counts conversations whose updatedAt falls within the
last ten minutes, but an update never set updatedAt, so it stayed at the
conversation's creation time: a run in a conversation older than ten minutes
was never counted. Starting a run now sets updatedAt.
* fix(db-mysql): read a string column's literal default back as its value
MySQL and OceanBase report a literal default in information_schema as the bare text, draft rather than 'draft'. The shared literal parser took it for an expression, so a character, enum or temporal column's default had no value once resolved, and a defaulted NOT NULL enum was listed as required in the API document's create schema on MySQL. Both inspectors now quote the bare value of those types, leaving numbers and CURRENT_TIMESTAMP as they are, and a portable contract checks on every dialect that a string and an enum field's default resolves to its value.
* fix(db-mysql): keep MariaDB and expression defaults out of the literal quoting
MariaDB 10.2.7 and later report column_default in the form it was declared,
'draft' for a literal and the bare word NULL for a nullable column without a
default, so quoting it again read 'draft' with its quotes and gave every such
column a "NULL" default. The inspector now reads version() with the columns and
passes a MariaDB default through, mapping its bare NULL to no default.
OceanBase reports default (uuid()) exactly as default 'uuid()', so a character
column's default is now told apart by reading the table's DDL, rather than
quoting an expression into a value the Repository would write.
A temporal default is quoted only when it starts like a date or a time, which
keeps curdate(), now() and CURRENT_TIMESTAMP expressions on every server, and
the temporal type list is taken from mysqlTypes instead of a second copy.
* fix(db-mysql): resolve the backslash escapes in a MariaDB default
MariaDB reports a string default escaped as MySQL writes literals, so
'back\\slash' and a text column's 'it\'s here' read back with the escape left
in them. Their literal now goes through the same unescaping as a MySQL
expression default before the shared parser reads it.
* fix(db-mysql): run the JSON filters on MariaDB and test the dialect against it
MariaDB stores JSON as text and has no cast(… as json), so every JSON equality
and membership filter failed to parse there. Both are now compared through
JSON_OVERLAPS, which MySQL and MariaDB evaluate alike: equality wraps each side
in a one-element array, so object keys compare in any order, array elements in
order, and 1 matches neither "1" nor true.
test:integration now runs the shared suite against MariaDB 10.11 after MySQL,
from a second service in docker-compose.yml, so CI covers both servers the
dialect serves. The isolation test reads tx_isolation where MariaDB before 11.1
has no transaction_isolation.
* fix(db-mysql): compare JSON equality both ways and keep OceanBase view expressions out of defaults
MariaDB compares two arrays inside JSON_OVERLAPS only as far as the second
one goes, so `[1, 2]` equalled `[1]` and every array equalled `[]`. Equality
now checks the overlap in both directions, which both servers evaluate
alike, and the JSON filter contract covers empty and partial containers.
The JSON filters need MySQL 8.0.17, as membership already did; the README
and changeset say so.
OceanBase reports `default (uuid())` and `default 'uuid()'` alike on a view
too, and a view's DDL declares no defaults, so a character default on a
view that reads like a function call is now read as an expression.
The MySQL integration runner no longer starts MariaDB after an interrupted
MySQL run, or while the MySQL database is kept.
* fix: answer a malformed task id with 403 on PostgreSQL, and keep the articles test portable to OceanBase
The notification example compared any path id with its uuid column, which PostgreSQL rejects with a 500 for a value like 'missing'; an id that is not a UUID now names no task. The examples template's articles migration test gives content in its raw insert, because OceanBase keeps no TEXT default in the table.
* fix(app-plugin-notification-example): match a task id in any case, and cover a malformed id on update
Validate the id with z.guid() instead of a hand-written pattern and lowercase it
before the query, so an uppercase id names the same task on SQLite and MySQL as
on dialects that compare uuid natively. Name Kingbase and MSSQL alongside
PostgreSQL as the dialects that reject a malformed uuid, and test PATCH with a
malformed id and GET with an uppercase one.
* docs: point page tests at app-testing/client, and test the Skill's page example with real translations
AGENTS.md and the AGENTS.md create-plugin generates name @nocobase/app-testing/client for page tests. The plugin-development Skill's page test example renders with the strict runtime from @nocobase/i18n/testing and the plugin's locale file instead of a mocked t, which let a missing or misspelt key pass. The example keeps its mocked services until @nocobase/app-testing is published.
* feat(create-plugin): prepare generated client plugins for page tests, and render the Skill's page test with renderWithApp
The Skill's settings page test renders inside a client application with
renderWithApp(), registers the real AuthorizationClient through services and
answers its permissions request with answerApi(), and shows the locale keys
the page needs, so it runs as written. A plugin create-plugin generates with
client code gets app-testing and the jsdom test packages, and a vitest.config.ts
that runs tests/client/ under jsdom; its AGENTS.md describes page tests in a
section of their own, including the plugins option. AGENTS.md no longer calls
renderWithApp()'s toaster the real one.
* fix(app-plugin-file): embed PDF previews as application/pdf and cover PDF in the Skill checklist
Refs TestManage problem #234.
* fix(app-plugin-file-example): embed PDF previews as application/pdf
Apply the Registry preview's PDF rule to the file example's preview
dialog: refuse a response carrying an active markup type and retype the
blob to application/pdf before embedding it. Clarify that retyping is the
safeguard, assert the frame source in the Registry test, and drop the
canvas-renderer history from the file Skill.
* refactor(app-plugin-file): share the PDF preview checks and tidy their tests
Use isActiveMarkupMimeType in the file example's previewKind instead of a
second inline list, name the Registry PDF preview error once, make the
example's discarded refusal message describe what happened, and type the
createObjectURL mock instead of asserting its argument.
* docs(app-plugin-file): separate browser and session failures in the PDF check
A login redirect during a PDF preview points at the session, not the
browser. Drop the example's unread refusal message and state the fixed
case precisely in the changeset.
* docs(app-plugin-file): state the PDF preview risk as external content URLs
* refactor(app-plugin-file): name the refused PDF response and tidy the MIME helpers
Report an HTML or XML response to the PDF preview with its own message so it
is distinguishable from a failed request, share MIME normalization in
file-preview.ts, move the example's active markup check into lib/mime.ts and
shorten the Skill's PDF guidance.
* docs(app-plugin-file): scope the PDF browser-limitation note to headless browsers
---------
Co-authored-by: gchust <5254737+gchust@users.noreply.github.com>
* fix(file): fall back to download when a previewed image fails to load
* fix(file): show the type icon in the image fallback instead of reloading the failed image
* fix(file): ship the image preview fallback in the Default template
Refresh the Default template's copy of the File Registry, which the
default-file-registry check requires, and add the template to the
changeset. Give ImagePreview a required url so a missing URL can no
longer match the initial failed-URL state, and replace the empty-URL
sentinel in DownloadFallback with an explicit FileThumbnail iconOnly prop.
The shell re-renders from the shared sidebar preference only once its subscription effect has run, so the test now waits for the collapsed and expanded states instead of asserting right after the click. Applied to all three templates.