pgkiln 0.84.0 → 0.85.0

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 (81) hide show
  1. package/docs/apex-feature-parity.md +2 -2
  2. package/docs/guide/03-builder.md +14 -5
  3. package/docs/guide/12-development.md +6 -1
  4. package/package.json +1 -1
  5. package/src/app.ts +9 -0
  6. package/src/builder/ai-agents.ts +86 -85
  7. package/src/builder/ai-builder.ts +30 -30
  8. package/src/builder/ai.ts +93 -92
  9. package/src/builder/api.ts +47 -47
  10. package/src/builder/app-groups.ts +33 -32
  11. package/src/builder/appfromfile.ts +57 -58
  12. package/src/builder/appsheets.ts +33 -32
  13. package/src/builder/appwizard.ts +65 -65
  14. package/src/builder/arrange.ts +23 -22
  15. package/src/builder/assistant.ts +39 -38
  16. package/src/builder/automations.ts +32 -31
  17. package/src/builder/availability.ts +24 -24
  18. package/src/builder/backups.ts +30 -31
  19. package/src/builder/blueprints.ts +62 -61
  20. package/src/builder/create-page.ts +52 -50
  21. package/src/builder/cross-app.ts +37 -35
  22. package/src/builder/dataload.ts +2 -2
  23. package/src/builder/datasets.ts +107 -106
  24. package/src/builder/dependencies.ts +20 -19
  25. package/src/builder/designer.ts +36 -36
  26. package/src/builder/diagnostics.ts +58 -57
  27. package/src/builder/features.ts +39 -35
  28. package/src/builder/feedback.ts +23 -20
  29. package/src/builder/find-workspace.ts +28 -24
  30. package/src/builder/form-designer.ts +36 -35
  31. package/src/builder/gallery.ts +25 -29
  32. package/src/builder/globalization.ts +50 -49
  33. package/src/builder/i18n/de.ts +3453 -0
  34. package/src/builder/i18n/en.ts +3453 -0
  35. package/src/builder/i18n/es.ts +3453 -0
  36. package/src/builder/i18n/fr.ts +3453 -0
  37. package/src/builder/i18n/it.ts +3453 -0
  38. package/src/builder/i18n/ja.ts +3453 -0
  39. package/src/builder/i18n/ko.ts +3453 -0
  40. package/src/builder/i18n/nl.ts +3453 -0
  41. package/src/builder/i18n/pt.ts +3453 -0
  42. package/src/builder/i18n/zh-cn.ts +3453 -0
  43. package/src/builder/i18n/zh-tw.ts +3453 -0
  44. package/src/builder/i18n.ts +17 -3
  45. package/src/builder/json-sources.ts +63 -62
  46. package/src/builder/layouts.ts +12 -11
  47. package/src/builder/locks.ts +29 -28
  48. package/src/builder/login-page.ts +26 -25
  49. package/src/builder/mail.ts +148 -147
  50. package/src/builder/newapp.ts +15 -12
  51. package/src/builder/plugins.ts +47 -44
  52. package/src/builder/process-jobs.ts +11 -10
  53. package/src/builder/pwa.ts +22 -21
  54. package/src/builder/remote-databases.ts +55 -56
  55. package/src/builder/report-settings.ts +1 -1
  56. package/src/builder/reporter.ts +43 -42
  57. package/src/builder/rest-catalogs.ts +87 -86
  58. package/src/builder/rest.ts +9 -8
  59. package/src/builder/routes.ts +27 -27
  60. package/src/builder/search.ts +14 -13
  61. package/src/builder/static-files.ts +31 -32
  62. package/src/builder/subscriptions.ts +47 -45
  63. package/src/builder/supporting.ts +23 -18
  64. package/src/builder/team.ts +179 -179
  65. package/src/builder/telemetry.ts +59 -62
  66. package/src/builder/templates.ts +66 -65
  67. package/src/builder/themeroller.ts +60 -63
  68. package/src/builder/ui-defaults.ts +21 -20
  69. package/src/builder/ui.ts +2 -1
  70. package/src/builder/upgrade.ts +25 -24
  71. package/src/builder/utilities.ts +48 -47
  72. package/src/builder/vector-providers.ts +67 -66
  73. package/src/builder/websources.ts +43 -42
  74. package/src/builder/wizards.ts +95 -89
  75. package/src/builder/workflow-migrate.ts +30 -31
  76. package/src/builder/workflows.ts +40 -39
  77. package/src/builder/workingcopies.ts +41 -49
  78. package/src/builder/workspace-backups.ts +21 -20
  79. package/src/builder/workspace-mail.ts +17 -16
  80. package/src/builder/workspace-resources.ts +104 -112
  81. package/src/builder/workspace-transfer.ts +34 -26
@@ -7,7 +7,7 @@ open item and open an issue or pull request; see [CONTRIBUTING.md](../CONTRIBUTI
7
7
  Legend: ✅ available · 🟡 partial (see notes) · ❌ not yet · ➖ not planned (a deliberate choice,
8
8
  or better served by the PostgreSQL ecosystem; see the notes and [extensions](guide/15-extensions.md))
9
9
 
10
- Last reviewed: 2026-11-06 against **APEX 26.2** (sprint 165: navigation menu badges from SQL, the notification bell and live badges; sprint 162: the SQL Workshop tools and the region and report settings in the eleven builder languages (CD-8); sprint 161: component events of grids, calendars, maps and trees (CD-3); sprint 160: the Workflow Diagram region, an instance's steps with the reached, current and failed ones marked (CD-7); sprint 159: map layers Lines, Polygons and Extruded Polygons with fill and line styles per layer or per row (CD-6); sprint 158: the item types Image Upload, Text Field with autocomplete, Percent Graph and Display Map (CD-5); sprint 157: the process types Close Dialog, Clear Session State, Reset Pagination, User Preferences, Server Side Geocoding, Print Report, Human Task – Create / – Manage and Form – Initialization (CD-4); sprint 156: dynamic action events double click, key down / press, mouse enter / leave, select, resize, scroll, page unload, before refresh and facets change, greater / less and not-in-list conditions, and the actions Cancel Event, Set Style, Collapse / Expand Tree, Show AI Assistant, Trigger Geocoding and Open report dialog (CD-3); sprint 155: APEX's validation types, with an authorization scheme, Always execute and the error's display location (CD-1); sprint 154: one catalogue of server-side condition types for every component, shown in words in the Page Designer (CD-2); sprint 153: Database users in the eleven builder languages (WS-7 leftover); sprint 152: Workspace utilities, the dashboard, Mail queue, Network access, Instance settings and LDAP directories in the eleven builder languages (WS-7 leftover); sprint 151: vector providers with a model in the database, PostgresML (APEX's ONNX models, AP-1 leftover); sprint 150: remote databases, another PostgreSQL database's tables as foreign tables the applications query (REST Enabled SQL, AP-7 leftover); sprint 149: running workflow instances inside parallel branches migrate to a newer version (WS-6 leftover); sprint 148: the AI assistant streams its answers as the model writes them (WS-4 leftover); sprint 147: `pgkiln advise --offline` checks an export's files without a database (AP-10 leftover); sprint 146: the Advisor checks JSON sources' views against their data profile (AP-8 leftover); sprint 145: the texts that offered AI with "Claude or OpenAI" name all four providers: Claude, OpenAI, Gemini and OpenAI-compatible services (AP-11); sprint 144: `pgkiln advise` for CI with text, JSON and SARIF output (APEXlang static analysis / SQLcl CodeScan), security checks in the Advisor, and working copies and locks from the command line and MCP (the Working Copy APIs) (AP-10); sprint 143: the interactive report's 26.2 dialogs: *is one of* / *is not one of* filters with the column's values and counts, *is between* two values (dates with the date picker in the application's format), relative periods on date columns, and the column order by drag and drop or arrow buttons, followed by downloads and the PDF (AP-9); sprint 142: JSON sources with a data profile and updatable views whose arrays are rows, document views of a table with its children (APEX's duality views), JSON path columns in reports (AP-8); sprint 141: static workspace files (`#WORKSPACE_FILES#`), remote servers for REST data sources, catalogs and AI services (an environment variable per environment), and workspace-level web credentials (AP-7); sprint 140: the items Shuttle (ordered), Select One and Select Many, the regions Classic Report (report templates, pagination types), Breadcrumb and Help Text with a page help text, and a [mapping of every APEX item and region type](#item-and-region-types-one-by-one) (AP-4); sprint 139: OpenTelemetry, traces with a span per SQL statement, web call, AI request and tool call, logs, metrics and `/metrics` for Prometheus, browser spans relayed by pgkiln, per workspace off or its own collector (AP-6); sprint 138: an application's Utilities tab with a dashboard, database object dependencies, change history per component, Upgrade application, UI defaults (APEX's Attribute Dictionary), application and page checksums and an icon browser (AP-5); sprint 137: Create page as a grid of page types with previews, the page types interactive report, smart filters, search page, data loading, tree, timeline, media list, content row, comments, dashboard, wizard, unified task list and workflow console, a Feature tab with nine features that install once, and Create page as copy with shared components mapped or copied (AP-3); sprint 136: vector providers (an AI service with an embedding model or a SQL function), `meta.embed`, *Add vector column* in Table Properties with an HNSW index and a queue that keeps embeddings current, search types vector and hybrid (reciprocal rank fusion with full-text) and a Retrieve (RAG) tool for AI agents, all as the app's role through RLS, on pgvector (AP-1); sprint 135: full-text search configurations, APEX's Oracle Text search type on PostgreSQL full-text search with stemming, ranking, highlights, *Did you mean* and *Add full-text search* in Table Properties (AP-2); sprint 134: the builder also in Italiano, 中文 (简体), 中文 (繁體), 日本語 and 한국어, APEX's ten builder languages plus Nederlands (SEC-5); sprints 121, 123–125: the builder in Deutsch, English, Español, Français, Nederlands and Português, step by step: the frame, sign-in, Workspace home, App Builder, Page Designer menus, the SQL Workshop's home, SQL Worksheet, Object Browser and Table Properties, Shared Components, application settings, users, identity providers, developers and workspaces (WS-7); sprint 122: *Ask in your own words* sets faceted search and smart filter facets, smart filters read a typed phrase (WS-5), and Sample Maps, Sample Approvals, Sample Data Loading and Sample Files in the App Gallery (WS-8); sprint 120: the App Gallery's single-topic sample apps (charts, cards, calendar, interactive grids, dynamic actions, reporting), the UX Pattern Catalog and a Utility Apps tab with Lookup Lists, Release Notes and Announcements (WS-8); sprint 119: data security per table, a roles × columns matrix and masked values per application role in a masking view, the same identity for pages, AI tools, REST, AutoREST and MCP (WS-13); sprint 118: Administration → Mail queue with delivery per hour and per workspace, the slowest provider calls and failures with Retry and Purge, a workspace's own mail service, and workspace network access lists that narrow the installation's allow-list (WS-9); sprint 117: Pillar Light and Pillar Dark navigation appearances with a header strip, clearer navigation tree states, subtle colour utilities, RTL; sprint 116: the comparison moved from 26.1 to 26.2, with a section for the 26.2 workspace items WS-1 … WS-13 and OCI managed identities; the App Builder list with Updated by, working copies and the lock; `pgkiln import --only`, a file-level import; stale rows on facet menus and blueprints fixed; sprint 115: popup LOVs that load on scroll, Wait Popup dynamic actions and dynamic actions on menu entries; sprint 114: Workflow 26.2 with custom task outcomes, task events and notifications, Send E-Mail and draft steps, migrating running instances and the diagram as SVG / PNG; sprint 113: Team Development with issues, labels, milestones, templates, a board and a set-up wizard; sprint 112: AI interactive reports with chips, Undo, computed columns and a phrase parser that needs no AI service; sprint 111: AI Agents with tools as a shared component, a Navigate tool with checksummed URLs, Gemini and OpenAI-compatible services; sprint 110: Data Reporter as a workspace tool with datasets, reporting applications, editors and viewers, published reports with stable URLs and pins; sprint 109: the Workspace home above the App Builder and SQL Workshop, with a summary, top applications, users and developers, and an optional release check; sprint 108: the code editor's syntax colours in the builder's light and dark themes, read-only SQL highlighted, the Data Modeler in dark mode; sprint 107: a transaction mode in the SQL Worksheet with Commit and Rollback; DA-18: JavaScript (MLE) not planned, plv8 where installed; sprint 106: the change log of DDL change sets with Flyway, Liquibase and SQL downloads; sprint 104: RESTful Services modules, templates and handlers with PL/pgSQL and media handlers, privileges, OpenAPI and Try it; sprint 103: Schema Export / Import with pg_dump and pg_restore; sprint 102: the SQL Monitor with live sessions, Top SQL and a statement's plan; sprint 101: Scheduling of pg_cron jobs with a schedule builder, run now and run history; sprint 100: RESTful Services, tables and views published as REST endpoints with paging, filters, keys and OpenAPI; sprint 99: Administration → Database Users for PostgreSQL roles; sprint 98: the Data Modeler with relational diagrams, DDL and a data dictionary; sprint 97: charts of a query in the worksheet, saved charts and their dashboard; sprint 96: the SQL Workshop's Launchpad with pins and tool groups, the builder's search with Ctrl+K; sprint 95: Table Properties: create, edit or copy a table in a form with a DDL preview and Apply in one transaction; sprint 94: an object's menu with Open in a panel, Quick DDL, Add index / trigger / policy and Drop with dependents; sprint 93: worksheets saved as SQL Scripts with tabs and a Files list, Format, Convert case, find and replace, bind and substitution variables; sprint 92: the worksheet's Query Result with sort, value dialog, copy and downloads, plans as a tree with the slowest step and misestimates, SQL History with status, search and run again; sprint 91: the SQL Worksheet like Database Actions' SQL: navigator, Run Statement at the cursor and Run Script with a log per statement, Explain Plan, Autotrace, notices, history, Run as an application role; sprint 90: Comment Out in the page designer, gallery labels in title case; sprint 89: the Object Browser's schema, one row of tabs, blank NULLs, Modify column; sprint 88: home cards with colour bands and round icons, the hero on the page background, a page footer with the version and Back to top; sprint 87: the Create Application wizard reloads the page list for a schema, RLS badges, the alias from the name; sprint 86: the page designer by page number, HTML error pages in the builder; sprint 85: the Form Designer, beyond APEX; sprint 84: the date picker, facet menus; sprint 83: Component Settings and Component Groups; sprint 82: Report Queries; sprint 81: Application Computations and Shortcuts in Shared Components; sprint 80: the master grid's first row selected when the page opens and after a search; grid cells as text with their format mask until edited, with cell navigation by arrow keys, Enter, F2 and Escape; sprint 79: generated dashboards with sums, donuts and lines, leaving out charts that say nothing; applications open in their own light or dark mode until the user chooses, per application; a user menu with Appearance and Sign in when signed out; the developer toolbar's App and Page texts and debug state; the report search's column scope in the magnifier; sprint 78: generated faceted search reports with one column per foreign key, the faceted search's total row count, Show all and Yes/No values, one search field, friendly headings and number formats in generated grids and master-detail pages, friendly names in the Create Application wizard's page list; sprint 77: the Page Designer's layout and tree by display position, button positions, the tree's context menu with Duplicate and Copy to other page, Show Common / Show All, Go to group and the Template Options dialog; display positions, button positions and side-column page templates at runtime; sprint 76: the interactive report's menus and pagination, date items in the date format; sprint 75: the SQL Workshop's Object Browser and tabs; sprint 74: the builder's information architecture: Shared Components grid, Settings tabs, application home search and tasks; sprint 73: the Create Application wizard on one page with page types and features; sprint 72: the runtime chrome compared with APEX 26.2's: the developer toolbar, the Iris look, the navigation bar's Help menu and Install button; sprint 71: generated applications compared with APEX 26.2's: labels, foreign key names in reports, file columns, format masks, the Home page and the dashboard; sprint 70: treemap, sunburst and timeline charts; sprint 69: dragging tasks on a Gantt chart; sprint 68: the Data Reporter's welcome page, Find my workspace; sprint 67: REST source catalogs from OpenAPI documents, a workspace's export and import (two rows added); sprint 66: a blueprint from an existing application; sprint 65: facet charts; sprint 64: approval vacation rules and expiration policies; sprint 63: range, box plot and stock charts; sprint 62: workspace preferences (SQL Workshop and RESTful services per workspace), extension links, Manage backups across applications, SQL Workshop utilities with object reports, About Database and a database monitor; sprint 61: the builder's screens compared with APEX 26.1: the workspace sign-in page, the Administration menu with workspace message, environment banner and utilization, the SQL Workshop home, the App Gallery, application groups and cross application reports (four rows added); sprint 60: isolated workspaces with their own database logins, row-level security on pgkiln's tables and workspace database roles, workspace administrators; sprint 59: the Page Designer's help pane, Quick edit from the developer toolbar; sprint 58: page groups; sprint 57: region templates hero, title bar, buttons container, inline dialog and drawer, wizard with wizard progress, the carousel; sprint 56: Generate DDL and Schema Comparison in the SQL Workshop, `pgkiln ddl` and `pgkiln compare`; sprint 55: an editable login page with a Login region and the Minimal page template, Forgot password through the application's mail service; sprint 54: interactive grid views, the single row view, group by and chart; sprint 53: application settings, user preferences and items kept per user, dynamic translations, `meta.data_export` and data export downloads, display image and geocoded address items; sprint 52: every dynamic action event and action, Ajax callbacks, an error handling function, the session timeout warning; sprint 51: checked imports, application backups, availability and Run only, feedback, type-specific attribute help; sprint 50: lost update detection, warn on unsaved changes, the URL and search regions with search configurations, the developer toolbar with a session state viewer, and e-mail through SMTP with OAuth2, Microsoft Graph, Amazon SES and HTTP APIs; import checks, application availability, property help and e-mail compared again; 2026-10-06: pgkiln 0.31.0, in progress: static application files and the Execute JavaScript dynamic action; region, item, dynamic action and process plug-ins; conditional and dynamic theme styles; XML and Excel in `meta.parse_data`, zips in SQL, `meta.v_boolean`; file items in S3-compatible object storage; map layers by the visible area and as vector tiles; a graphical query builder; tenants for workflows and tasks; application files as YAML and region static IDs; Lucide icons and icon modifiers; PWA push notifications; 0.30.0: 136 icons and an icon picker; an axe-core accessibility audit; cropping pictures; session sharing between applications; twenty-two built-in languages; 0.29.0: workspaces; the Iris base style; drawers; built-in template components including the metric card; Theme Roller dark-mode colours, live preview and template options on items and report columns; report selection across pages; instance settings; fourteen built-in languages; 0.28.0: AI with Claude and OpenAI; 0.27.0: Data Reporter; sample data generator; create application from several sheets, pasted data or existing tables; 0.26.0: working copies with a three-way merge; theme, library and boilerplate applications with subscriptions; 0.25.0: Gantt, pyramid and polar charts; map layers, clustering and server-side spatial filters; REST data sources that write back from forms and grids and synchronise into tables, OAuth2 password and refresh-token grants; debug messages per request and an install/upgrade log; `meta.web_request()` and `meta.parse_data()`; Theme Roller style variants and template options; 0.24.0: automations with several actions, error handling per row and runs from SQL; invoke-API workflow steps; Unload Data in the Data Workshop; create page wizards for cards, calendar, chart, map, faceted search, form and master-detail; create an application from a file; 0.23.0: interactive grid aggregates, frozen/moved/resized columns, layouts and saved grid reports, master-detail, row actions and copy/paste; download processes, execution chains in the background, workflow processes, branches to a function's URL or another app, the Dialog Closed event; number format masks, automatic time zone, runtime messages in German, French and Spanish; SQL scripts, Quick SQL, a query builder, XML loading and data load definitions; custom authentication, lists, page and application locks with developer comments, supporting objects; 0.22.0: keyset paging, report PDFs and REST collections streamed from a cursor, database-account authentication; 0.21.0: HTTP-header authentication behind a reverse proxy; 0.20.0: a popup list of values with server-side search; 0.19.0: row-range pagination, maximum row counts and row limits, lazy loading and caching of regions, streamed CSV/Excel downloads; 0.18.0: calendar week/day/list views with drag and drop, bubble/gauge/funnel/radar charts and drill-down, smart filters, a region display selector and more facet types, computations, conditional branches, build options, menu buttons, REST data sources and web credentials, rich text/Markdown, rating, combobox, date range and QR code items; 0.17.0: an App Builder home, workspace dashboard and utilities like APEX's; 0.16.0: a Page Designer with drag-and-drop layout, a code editor with autocomplete, template components and plug-ins, parallel branches and versions of workflows, the `pgkiln` command line with one file per component; 0.15.0: drop and paste files, heat maps, filtering a report by the map area; 0.14.0: stacked, combo, scatter and pie charts, several files per upload item; 0.13.0: map and tree regions; 0.12.0: REST modules in the builder; 0.11.0: workflows and Progressive Web Apps; 0.10.0: LDAP, SAML, "Keep me signed in", document templates, JSON loading, approvals and the task list; pgkiln installs no application, HR is an example).
10
+ Last reviewed: 2026-11-06 against **APEX 26.2** (sprint 164: every App Builder screen in the eleven builder languages (WS-7 leftover); sprint 165: navigation menu badges from SQL, the notification bell and live badges; sprint 162: the SQL Workshop tools and the region and report settings in the eleven builder languages (CD-8); sprint 161: component events of grids, calendars, maps and trees (CD-3); sprint 160: the Workflow Diagram region, an instance's steps with the reached, current and failed ones marked (CD-7); sprint 159: map layers Lines, Polygons and Extruded Polygons with fill and line styles per layer or per row (CD-6); sprint 158: the item types Image Upload, Text Field with autocomplete, Percent Graph and Display Map (CD-5); sprint 157: the process types Close Dialog, Clear Session State, Reset Pagination, User Preferences, Server Side Geocoding, Print Report, Human Task – Create / – Manage and Form – Initialization (CD-4); sprint 156: dynamic action events double click, key down / press, mouse enter / leave, select, resize, scroll, page unload, before refresh and facets change, greater / less and not-in-list conditions, and the actions Cancel Event, Set Style, Collapse / Expand Tree, Show AI Assistant, Trigger Geocoding and Open report dialog (CD-3); sprint 155: APEX's validation types, with an authorization scheme, Always execute and the error's display location (CD-1); sprint 154: one catalogue of server-side condition types for every component, shown in words in the Page Designer (CD-2); sprint 153: Database users in the eleven builder languages (WS-7 leftover); sprint 152: Workspace utilities, the dashboard, Mail queue, Network access, Instance settings and LDAP directories in the eleven builder languages (WS-7 leftover); sprint 151: vector providers with a model in the database, PostgresML (APEX's ONNX models, AP-1 leftover); sprint 150: remote databases, another PostgreSQL database's tables as foreign tables the applications query (REST Enabled SQL, AP-7 leftover); sprint 149: running workflow instances inside parallel branches migrate to a newer version (WS-6 leftover); sprint 148: the AI assistant streams its answers as the model writes them (WS-4 leftover); sprint 147: `pgkiln advise --offline` checks an export's files without a database (AP-10 leftover); sprint 146: the Advisor checks JSON sources' views against their data profile (AP-8 leftover); sprint 145: the texts that offered AI with "Claude or OpenAI" name all four providers: Claude, OpenAI, Gemini and OpenAI-compatible services (AP-11); sprint 144: `pgkiln advise` for CI with text, JSON and SARIF output (APEXlang static analysis / SQLcl CodeScan), security checks in the Advisor, and working copies and locks from the command line and MCP (the Working Copy APIs) (AP-10); sprint 143: the interactive report's 26.2 dialogs: *is one of* / *is not one of* filters with the column's values and counts, *is between* two values (dates with the date picker in the application's format), relative periods on date columns, and the column order by drag and drop or arrow buttons, followed by downloads and the PDF (AP-9); sprint 142: JSON sources with a data profile and updatable views whose arrays are rows, document views of a table with its children (APEX's duality views), JSON path columns in reports (AP-8); sprint 141: static workspace files (`#WORKSPACE_FILES#`), remote servers for REST data sources, catalogs and AI services (an environment variable per environment), and workspace-level web credentials (AP-7); sprint 140: the items Shuttle (ordered), Select One and Select Many, the regions Classic Report (report templates, pagination types), Breadcrumb and Help Text with a page help text, and a [mapping of every APEX item and region type](#item-and-region-types-one-by-one) (AP-4); sprint 139: OpenTelemetry, traces with a span per SQL statement, web call, AI request and tool call, logs, metrics and `/metrics` for Prometheus, browser spans relayed by pgkiln, per workspace off or its own collector (AP-6); sprint 138: an application's Utilities tab with a dashboard, database object dependencies, change history per component, Upgrade application, UI defaults (APEX's Attribute Dictionary), application and page checksums and an icon browser (AP-5); sprint 137: Create page as a grid of page types with previews, the page types interactive report, smart filters, search page, data loading, tree, timeline, media list, content row, comments, dashboard, wizard, unified task list and workflow console, a Feature tab with nine features that install once, and Create page as copy with shared components mapped or copied (AP-3); sprint 136: vector providers (an AI service with an embedding model or a SQL function), `meta.embed`, *Add vector column* in Table Properties with an HNSW index and a queue that keeps embeddings current, search types vector and hybrid (reciprocal rank fusion with full-text) and a Retrieve (RAG) tool for AI agents, all as the app's role through RLS, on pgvector (AP-1); sprint 135: full-text search configurations, APEX's Oracle Text search type on PostgreSQL full-text search with stemming, ranking, highlights, *Did you mean* and *Add full-text search* in Table Properties (AP-2); sprint 134: the builder also in Italiano, 中文 (简体), 中文 (繁體), 日本語 and 한국어, APEX's ten builder languages plus Nederlands (SEC-5); sprints 121, 123–125: the builder in Deutsch, English, Español, Français, Nederlands and Português, step by step: the frame, sign-in, Workspace home, App Builder, Page Designer menus, the SQL Workshop's home, SQL Worksheet, Object Browser and Table Properties, Shared Components, application settings, users, identity providers, developers and workspaces (WS-7); sprint 122: *Ask in your own words* sets faceted search and smart filter facets, smart filters read a typed phrase (WS-5), and Sample Maps, Sample Approvals, Sample Data Loading and Sample Files in the App Gallery (WS-8); sprint 120: the App Gallery's single-topic sample apps (charts, cards, calendar, interactive grids, dynamic actions, reporting), the UX Pattern Catalog and a Utility Apps tab with Lookup Lists, Release Notes and Announcements (WS-8); sprint 119: data security per table, a roles × columns matrix and masked values per application role in a masking view, the same identity for pages, AI tools, REST, AutoREST and MCP (WS-13); sprint 118: Administration → Mail queue with delivery per hour and per workspace, the slowest provider calls and failures with Retry and Purge, a workspace's own mail service, and workspace network access lists that narrow the installation's allow-list (WS-9); sprint 117: Pillar Light and Pillar Dark navigation appearances with a header strip, clearer navigation tree states, subtle colour utilities, RTL; sprint 116: the comparison moved from 26.1 to 26.2, with a section for the 26.2 workspace items WS-1 … WS-13 and OCI managed identities; the App Builder list with Updated by, working copies and the lock; `pgkiln import --only`, a file-level import; stale rows on facet menus and blueprints fixed; sprint 115: popup LOVs that load on scroll, Wait Popup dynamic actions and dynamic actions on menu entries; sprint 114: Workflow 26.2 with custom task outcomes, task events and notifications, Send E-Mail and draft steps, migrating running instances and the diagram as SVG / PNG; sprint 113: Team Development with issues, labels, milestones, templates, a board and a set-up wizard; sprint 112: AI interactive reports with chips, Undo, computed columns and a phrase parser that needs no AI service; sprint 111: AI Agents with tools as a shared component, a Navigate tool with checksummed URLs, Gemini and OpenAI-compatible services; sprint 110: Data Reporter as a workspace tool with datasets, reporting applications, editors and viewers, published reports with stable URLs and pins; sprint 109: the Workspace home above the App Builder and SQL Workshop, with a summary, top applications, users and developers, and an optional release check; sprint 108: the code editor's syntax colours in the builder's light and dark themes, read-only SQL highlighted, the Data Modeler in dark mode; sprint 107: a transaction mode in the SQL Worksheet with Commit and Rollback; DA-18: JavaScript (MLE) not planned, plv8 where installed; sprint 106: the change log of DDL change sets with Flyway, Liquibase and SQL downloads; sprint 104: RESTful Services modules, templates and handlers with PL/pgSQL and media handlers, privileges, OpenAPI and Try it; sprint 103: Schema Export / Import with pg_dump and pg_restore; sprint 102: the SQL Monitor with live sessions, Top SQL and a statement's plan; sprint 101: Scheduling of pg_cron jobs with a schedule builder, run now and run history; sprint 100: RESTful Services, tables and views published as REST endpoints with paging, filters, keys and OpenAPI; sprint 99: Administration → Database Users for PostgreSQL roles; sprint 98: the Data Modeler with relational diagrams, DDL and a data dictionary; sprint 97: charts of a query in the worksheet, saved charts and their dashboard; sprint 96: the SQL Workshop's Launchpad with pins and tool groups, the builder's search with Ctrl+K; sprint 95: Table Properties: create, edit or copy a table in a form with a DDL preview and Apply in one transaction; sprint 94: an object's menu with Open in a panel, Quick DDL, Add index / trigger / policy and Drop with dependents; sprint 93: worksheets saved as SQL Scripts with tabs and a Files list, Format, Convert case, find and replace, bind and substitution variables; sprint 92: the worksheet's Query Result with sort, value dialog, copy and downloads, plans as a tree with the slowest step and misestimates, SQL History with status, search and run again; sprint 91: the SQL Worksheet like Database Actions' SQL: navigator, Run Statement at the cursor and Run Script with a log per statement, Explain Plan, Autotrace, notices, history, Run as an application role; sprint 90: Comment Out in the page designer, gallery labels in title case; sprint 89: the Object Browser's schema, one row of tabs, blank NULLs, Modify column; sprint 88: home cards with colour bands and round icons, the hero on the page background, a page footer with the version and Back to top; sprint 87: the Create Application wizard reloads the page list for a schema, RLS badges, the alias from the name; sprint 86: the page designer by page number, HTML error pages in the builder; sprint 85: the Form Designer, beyond APEX; sprint 84: the date picker, facet menus; sprint 83: Component Settings and Component Groups; sprint 82: Report Queries; sprint 81: Application Computations and Shortcuts in Shared Components; sprint 80: the master grid's first row selected when the page opens and after a search; grid cells as text with their format mask until edited, with cell navigation by arrow keys, Enter, F2 and Escape; sprint 79: generated dashboards with sums, donuts and lines, leaving out charts that say nothing; applications open in their own light or dark mode until the user chooses, per application; a user menu with Appearance and Sign in when signed out; the developer toolbar's App and Page texts and debug state; the report search's column scope in the magnifier; sprint 78: generated faceted search reports with one column per foreign key, the faceted search's total row count, Show all and Yes/No values, one search field, friendly headings and number formats in generated grids and master-detail pages, friendly names in the Create Application wizard's page list; sprint 77: the Page Designer's layout and tree by display position, button positions, the tree's context menu with Duplicate and Copy to other page, Show Common / Show All, Go to group and the Template Options dialog; display positions, button positions and side-column page templates at runtime; sprint 76: the interactive report's menus and pagination, date items in the date format; sprint 75: the SQL Workshop's Object Browser and tabs; sprint 74: the builder's information architecture: Shared Components grid, Settings tabs, application home search and tasks; sprint 73: the Create Application wizard on one page with page types and features; sprint 72: the runtime chrome compared with APEX 26.2's: the developer toolbar, the Iris look, the navigation bar's Help menu and Install button; sprint 71: generated applications compared with APEX 26.2's: labels, foreign key names in reports, file columns, format masks, the Home page and the dashboard; sprint 70: treemap, sunburst and timeline charts; sprint 69: dragging tasks on a Gantt chart; sprint 68: the Data Reporter's welcome page, Find my workspace; sprint 67: REST source catalogs from OpenAPI documents, a workspace's export and import (two rows added); sprint 66: a blueprint from an existing application; sprint 65: facet charts; sprint 64: approval vacation rules and expiration policies; sprint 63: range, box plot and stock charts; sprint 62: workspace preferences (SQL Workshop and RESTful services per workspace), extension links, Manage backups across applications, SQL Workshop utilities with object reports, About Database and a database monitor; sprint 61: the builder's screens compared with APEX 26.1: the workspace sign-in page, the Administration menu with workspace message, environment banner and utilization, the SQL Workshop home, the App Gallery, application groups and cross application reports (four rows added); sprint 60: isolated workspaces with their own database logins, row-level security on pgkiln's tables and workspace database roles, workspace administrators; sprint 59: the Page Designer's help pane, Quick edit from the developer toolbar; sprint 58: page groups; sprint 57: region templates hero, title bar, buttons container, inline dialog and drawer, wizard with wizard progress, the carousel; sprint 56: Generate DDL and Schema Comparison in the SQL Workshop, `pgkiln ddl` and `pgkiln compare`; sprint 55: an editable login page with a Login region and the Minimal page template, Forgot password through the application's mail service; sprint 54: interactive grid views, the single row view, group by and chart; sprint 53: application settings, user preferences and items kept per user, dynamic translations, `meta.data_export` and data export downloads, display image and geocoded address items; sprint 52: every dynamic action event and action, Ajax callbacks, an error handling function, the session timeout warning; sprint 51: checked imports, application backups, availability and Run only, feedback, type-specific attribute help; sprint 50: lost update detection, warn on unsaved changes, the URL and search regions with search configurations, the developer toolbar with a session state viewer, and e-mail through SMTP with OAuth2, Microsoft Graph, Amazon SES and HTTP APIs; import checks, application availability, property help and e-mail compared again; 2026-10-06: pgkiln 0.31.0, in progress: static application files and the Execute JavaScript dynamic action; region, item, dynamic action and process plug-ins; conditional and dynamic theme styles; XML and Excel in `meta.parse_data`, zips in SQL, `meta.v_boolean`; file items in S3-compatible object storage; map layers by the visible area and as vector tiles; a graphical query builder; tenants for workflows and tasks; application files as YAML and region static IDs; Lucide icons and icon modifiers; PWA push notifications; 0.30.0: 136 icons and an icon picker; an axe-core accessibility audit; cropping pictures; session sharing between applications; twenty-two built-in languages; 0.29.0: workspaces; the Iris base style; drawers; built-in template components including the metric card; Theme Roller dark-mode colours, live preview and template options on items and report columns; report selection across pages; instance settings; fourteen built-in languages; 0.28.0: AI with Claude and OpenAI; 0.27.0: Data Reporter; sample data generator; create application from several sheets, pasted data or existing tables; 0.26.0: working copies with a three-way merge; theme, library and boilerplate applications with subscriptions; 0.25.0: Gantt, pyramid and polar charts; map layers, clustering and server-side spatial filters; REST data sources that write back from forms and grids and synchronise into tables, OAuth2 password and refresh-token grants; debug messages per request and an install/upgrade log; `meta.web_request()` and `meta.parse_data()`; Theme Roller style variants and template options; 0.24.0: automations with several actions, error handling per row and runs from SQL; invoke-API workflow steps; Unload Data in the Data Workshop; create page wizards for cards, calendar, chart, map, faceted search, form and master-detail; create an application from a file; 0.23.0: interactive grid aggregates, frozen/moved/resized columns, layouts and saved grid reports, master-detail, row actions and copy/paste; download processes, execution chains in the background, workflow processes, branches to a function's URL or another app, the Dialog Closed event; number format masks, automatic time zone, runtime messages in German, French and Spanish; SQL scripts, Quick SQL, a query builder, XML loading and data load definitions; custom authentication, lists, page and application locks with developer comments, supporting objects; 0.22.0: keyset paging, report PDFs and REST collections streamed from a cursor, database-account authentication; 0.21.0: HTTP-header authentication behind a reverse proxy; 0.20.0: a popup list of values with server-side search; 0.19.0: row-range pagination, maximum row counts and row limits, lazy loading and caching of regions, streamed CSV/Excel downloads; 0.18.0: calendar week/day/list views with drag and drop, bubble/gauge/funnel/radar charts and drill-down, smart filters, a region display selector and more facet types, computations, conditional branches, build options, menu buttons, REST data sources and web credentials, rich text/Markdown, rating, combobox, date range and QR code items; 0.17.0: an App Builder home, workspace dashboard and utilities like APEX's; 0.16.0: a Page Designer with drag-and-drop layout, a code editor with autocomplete, template components and plug-ins, parallel branches and versions of workflows, the `pgkiln` command line with one file per component; 0.15.0: drop and paste files, heat maps, filtering a report by the map area; 0.14.0: stacked, combo, scatter and pie charts, several files per upload item; 0.13.0: map and tree regions; 0.12.0: REST modules in the builder; 0.11.0: workflows and Progressive Web Apps; 0.10.0: LDAP, SAML, "Keep me signed in", document templates, JSON loading, approvals and the task list; pgkiln installs no application, HR is an example).
11
11
 
12
12
  ## At a glance
13
13
 
@@ -336,7 +336,7 @@ row above where there is one.
336
336
  | WS-4 AI Agents with tools | ✅ | (sprint 111) AI Agents and AI Tools as shared components, a Navigate tool, Gemini and OpenAI-compatible services ([Workflow, automation and AI](#workflow-automation-and-ai)) |
337
337
  | WS-5 AI interactive reports: chips, computed columns | ✅ | (sprint 112) *Ask in your own words* with chips, Undo and computed columns; a phrase parser that needs no AI service; (sprint 122) phrases set faceted search and smart filter facets |
338
338
  | WS-6 Workflow 26.2: outcomes, migrating running instances, drafts, diagram export | ✅ | (sprint 114) Custom task outcomes, task events, Send E-Mail and draft steps, migrating running instances, the diagram as SVG / PNG ([Workflow, automation and AI](#workflow-automation-and-ai)) |
339
- | WS-7 The builder in more languages | ✅ | (sprint 121) The builder in Deutsch, English, Español, Français, Nederlands and Português, and (sprint 134, SEC-5) Italiano, 中文 (简体), 中文 (繁體), 日本語 and 한국어 with CJK font stacks: links on the sign-in page, a language menu in the status bar, the choice stored per developer (migration 127); builder texts are keyed message files with a coverage check in CI, also for the builder's own JavaScript (sprint 123). Translated (sprints 121, 123–125): the builder's frame, sign-in page, Workspace home, App Builder home, an application's home and tabs, the Page Designer's toolbar, menus, tree and gallery, the SQL Workshop's home and tabs, the SQL Worksheet, Object Browser and Table Properties, Shared Components, an application's settings, Users, identity providers, Developers and Workspaces ([chapter 3](guide/03-builder.md#choosing-the-builder-language)). (sprint 152) Workspace utilities, the dashboard, Mail queue, Network access, Instance settings and LDAP directories; (sprint 153) Database users; (sprint 162, CD-8) every SQL Workshop tool (SQL Monitor, Top SQL, Quick SQL, SQL Scripts, Generate DDL, Schema Comparison, Change Log, Charts, Load / Unload Data, Sample Data, Query Builder, Data Modeler, Utilities, Object Reports, AI, Schema Export / Import, Scheduling with schedules in words, RESTful Services, data security, document views) and the region and report settings. Still English: some App Builder screens (Team Development, mail, datasets, JSON sources, blueprints and a few more) until they move over; messages of checks shared with the command line; by design the property editor's and component forms' field names and help |
339
+ | WS-7 The builder in more languages | ✅ | (sprint 121) The builder in Deutsch, English, Español, Français, Nederlands and Português, and (sprint 134, SEC-5) Italiano, 中文 (简体), 中文 (繁體), 日本語 and 한국어 with CJK font stacks: links on the sign-in page, a language menu in the status bar, the choice stored per developer (migration 127); builder texts are keyed message files with a coverage check in CI, also for the builder's own JavaScript (sprint 123). Translated (sprints 121, 123–125): the builder's frame, sign-in page, Workspace home, App Builder home, an application's home and tabs, the Page Designer's toolbar, menus, tree and gallery, the SQL Workshop's home and tabs, the SQL Worksheet, Object Browser and Table Properties, Shared Components, an application's settings, Users, identity providers, Developers and Workspaces ([chapter 3](guide/03-builder.md#choosing-the-builder-language)). (sprint 152) Workspace utilities, the dashboard, Mail queue, Network access, Instance settings and LDAP directories; (sprint 153) Database users; (sprint 162, CD-8) every SQL Workshop tool (SQL Monitor, Top SQL, Quick SQL, SQL Scripts, Generate DDL, Schema Comparison, Change Log, Charts, Load / Unload Data, Sample Data, Query Builder, Data Modeler, Utilities, Object Reports, AI, Schema Export / Import, Scheduling with schedules in words, RESTful Services, data security, document views) and the region and report settings; (sprint 164) every other App Builder screen (Team Development, mail services, datasets, REST sources, AI services and agents, blueprints, JSON sources, the wizards, Theme Roller, Form Designer, working copies, locks, automations, workflows, plug-ins, the App Gallery, backups, availability, supporting objects and the rest). Still English: messages of checks shared with the command line (the Advisor, policy checks); by design the property editor's and component forms' field names and help |
340
340
  | WS-8 App Gallery: more sample apps, Utility Apps | ✅ | (sprint 120) Four tabs with counts and a search that also works without JavaScript ([chapter 3](guide/03-builder.md#app-gallery)). *Sample Apps*: HR Demo, Tasks, ten single-topic apps (Sample Charts, Sample Cards, Sample Calendar, Sample Interactive Grids, Sample Dynamic Actions, Sample Reporting; sprint 122: Sample Maps, Sample Approvals, Sample Data Loading, Sample Files) and the **UX Pattern Catalog**, each with its own schema, data, demo account and explanatory texts, installed by administrators from one SQL file in one transaction; *Utility Apps*: Lookup Lists, Release Notes and Announcements as blueprints any developer installs into the workspace. APEX's 27 sample apps include topics pgkiln shows in HR Demo instead (REST, trees, collections); no remote catalogue |
341
341
  | WS-9 Mail queue reporting, workspace SMTP, workspace network ACLs | ✅ | (sprint 118) Administration → Mail queue (per hour and per workspace / application, the slowest provider calls, failures with the provider's answer, Retry and Purge), a workspace's own mail service, and a workspace's network access list that narrows the installation's allow-list for every outgoing call ([Administration](#administration)) |
342
342
  | WS-10 Universal Theme 26.2: Pillar appearances, navigation polish | ✅ | (sprint 117) Theme Roller → Navigation appearance Pillar Light / Pillar Dark per application or style, with a header strip colour for light and dark mode; the current page filled, its parents bold with an accent guide line; `u-color-*-subtle` utilities; a lighter processing indicator; RTL mirrored |
@@ -528,11 +528,20 @@ SQL Monitor, Top SQL, Quick SQL, SQL Scripts, Generate DDL, Schema Comparison, C
528
528
  Data, Sample Data, Query Builder, Data Modeler, Utilities, Object Reports, the SQL Workshop's AI, Schema Export /
529
529
  Import, Scheduling (schedules are described in your language), RESTful Services (modules and AutoREST) and the
530
530
  Object Browser's data security and document view, and the Page Designer's **region and report settings** (cards,
531
- charts, maps, calendars, facets, trees and the other region types, a report's columns). Messages that come from
532
- the database or from checks shared with the command line (a policy check, a REST handler's validation) stay
533
- English. Other builder pages are still in English (they move over screen by screen); so are the
534
- Page Designer's property names and their help (and, the same way, the field names and help of a shared component's
535
- form), the links to this guide, and any SQL the builder generates.
531
+ charts, maps, calendars, facets, trees and the other region types, a report's columns), and (sprint 164) every
532
+ other **App Builder screen**: Team Development, mail services, Data Reporter datasets, REST data sources and
533
+ catalogs, workspace files, remote servers and web credentials, template components, vector providers, AI services,
534
+ the AI assistant and AI agents, blueprints, JSON sources, the REST API page, globalization, remote databases, debug
535
+ messages and the installation log, sign-in settings, the Create Application and create page wizards, Theme Roller,
536
+ features, OpenTelemetry, the Page Designer's layout messages, the Form Designer, utilities, subscriptions, working
537
+ copies, application groups, *Find my workspace* (its mail too, in the language of the page it was asked from),
538
+ static application files, page and application locks with developer comments, automations, workflow versions and
539
+ the migration of running instances, plug-ins, the App Gallery, PWA settings, the builder's search, backups and
540
+ workspace backups, availability, feedback, cross-application reports, workspace export / import, upgrade
541
+ application, page layouts, dependencies, UI defaults and supporting objects. Messages that come from the database
542
+ or from checks shared with the command line (a policy check, the Advisor, a REST handler's validation) stay
543
+ English, and so, by design, do the Page Designer's property names and their help (and, the same way, the field
544
+ names and help of a shared component's form), the links to this guide, and any SQL the builder generates.
536
545
 
537
546
  ## Workspace home
538
547
 
@@ -344,6 +344,7 @@ src/
344
344
  i18n.ts (sprint 121) the builder's own texts: BUILDER_LANGUAGES, tr(s) / translatorFor(lang) → tx(key, params),
345
345
  tx.rich (markup in a sentence), tx.dyn (keys made at run time), tx.label (known breadcrumb labels),
346
346
  (sprint 162) tx.code (`backticks` become <code>), withTranslator / scopedTx (a request-scoped translator)
347
+ (sprint 164) inRequestScope / setRequestTranslator: scopedTx in every builder handler
347
348
  i18n/ en.ts defines the builder's keys; de, es, fr, it, nl, pt, zh-cn, zh-tw, ja, ko (sprint 134) are Records of the same keys
348
349
  language.ts (migration 127) the status bar's language menu (POST /builder/language), the sign-in page's
349
350
  language links (?lang=, a cookie), meta.developer.language
@@ -719,7 +720,11 @@ tx.code('cr.schedule_help'); // (sprint 162) `backticked` pa
719
720
  pass `tx` to each of them: its entry point runs inside `withTranslator(tr(s), () => …)`, and the helpers import
720
721
  `scopedTx as tx`, which takes the translator of the request being rendered from an `AsyncLocalStorage` (so two
721
722
  requests in different languages never mix; `test/builder-i18n.test.ts` renders German and French concurrently).
722
- Outside `withTranslator` it is English.
723
+ (sprint 164) Every `/builder` route runs in a request scope (`inRequestScope` in `app.ts`), and `developer()` sets
724
+ the developer's translator in it once the session is loaded (`setRequestTranslator`), so `scopedTx` works in any
725
+ builder handler and the helpers it calls without `withTranslator`; before sign-in it follows the language cookie
726
+ (`cookieLanguage`). Outside a request (the command line, MCP, background jobs) it is English. Plural counts that
727
+ English writes differently get a separate `_1` key, so the English stays exactly as before.
723
728
 
724
729
  A missing text falls back to English, never to the raw key. Shared helpers that render chrome take the session as
725
730
  an optional last argument (`appHeader(a, 'pages', s)`, `workshopTabs('sql', s)`); `shell()` translates known English
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgkiln",
3
- "version": "0.84.0",
3
+ "version": "0.85.0",
4
4
  "description": "Open source low-code, SQL-driven application builder for PostgreSQL, inspired by Oracle APEX",
5
5
  "keywords": [
6
6
  "postgresql",
package/src/app.ts CHANGED
@@ -106,6 +106,7 @@ import { runtimeScope } from './db.ts';
106
106
  import { startTelemetry } from './telemetry/export.ts';
107
107
  import { telemetryHooks, telemetryRoutes } from './telemetry/http.ts';
108
108
  import { telemetryBuilderRoutes } from './builder/telemetry.ts';
109
+ import { inRequestScope } from './builder/i18n.ts';
109
110
 
110
111
  export async function buildApp(opts: { logger?: boolean } = {}) {
111
112
  // a database older than the code: apply the migrations (MIGRATE_ON_START=true)
@@ -209,6 +210,14 @@ export async function buildApp(opts: { logger?: boolean } = {}) {
209
210
  // the page's URL carries the package version, so the file never changes under it
210
211
  return reply.header('cache-control', 'public, max-age=31536000, immutable').type('image/svg+xml').send(svg);
211
212
  });
213
+ // (sprint 164) builder handlers run in a request scope: scopedTx (src/builder/i18n.ts) takes the developer's language from it
214
+ app.addHook('onRoute', (opts) => {
215
+ if (!opts.url.startsWith('/builder')) return;
216
+ const handler = opts.handler;
217
+ opts.handler = function (this: unknown, req, reply) {
218
+ return inRequestScope(req, () => (handler as (this: unknown, ...a: unknown[]) => unknown).call(this, req, reply));
219
+ } as typeof opts.handler;
220
+ });
212
221
  await app.register(runtimeRoutes);
213
222
  await app.register(accountRoutes);
214
223
  await app.register(notificationRoutes);
@@ -7,6 +7,7 @@ import { agentProblems, aiToolProblems, definitionProblems, EFFORTS, type AiAgen
7
7
  import type { Session } from '../session.ts';
8
8
  import { appOr404 } from './forms.ts';
9
9
  import { appHeader, back, BASE, csrf, developer, flash, input, region, select, send, shell, type Req } from './ui.ts';
10
+ import { scopedTx as tx } from './i18n.ts';
10
11
 
11
12
  // Shared Components → Generative AI → AI Agents (migration 121): the
12
13
  // application's AI tools and AI agents, kept as two JSON lists in meta.app
@@ -18,13 +19,13 @@ type Body = Record<string, string | string[] | undefined>;
18
19
  const one = (v: string | string[] | undefined) => (Array.isArray(v) ? v[0] : v) ?? '';
19
20
  const all = (v: string | string[] | undefined) => (v === undefined ? [] : [v].flat());
20
21
 
21
- const TOOL_TYPES: [string, string][] = [
22
- ['sql', 'SQL query (read-only, as the application\'s role)'],
23
- ['function', 'PL/pgSQL function (may change data)'],
24
- ['rest', 'REST data source'],
25
- ['javascript', 'JavaScript action in the browser'],
26
- ['navigate', 'Navigate: a link to a page'],
27
- ['retrieve', 'Retrieve (RAG): rows of a search configuration'],
22
+ const TOOL_TYPES = (): [string, string][] => [
23
+ ['sql', tx('agt.type.sql')],
24
+ ['function', tx('agt.type.function')],
25
+ ['rest', tx('agt.type.rest')],
26
+ ['javascript', tx('agt.type.javascript')],
27
+ ['navigate', tx('agt.type.navigate')],
28
+ ['retrieve', tx('agt.type.retrieve')],
28
29
  ];
29
30
 
30
31
  const when = (d: Date | string) => new Date(d).toISOString().slice(0, 19).replace('T', ' ');
@@ -36,12 +37,12 @@ async function schemes(appId: number) {
36
37
  }
37
38
 
38
39
  const authzSelect = (names: string[], cur: unknown, help: string) =>
39
- select('authz', 'Authorization scheme', cur ?? '', [['', '- none: everyone who may use the page -'], ['MUST_NOT_BE_PUBLIC_USER', 'Must not be the public user'], ...names.map((n): [string, string] => [n, n]),
40
+ select('authz', tx('agt.authorization_scheme'), cur ?? '', [['', tx('agt.none_everyone_who_may_use')], ['MUST_NOT_BE_PUBLIC_USER', tx('agt.must_not_be_the_public')], ...names.map((n): [string, string] => [n, n]),
40
41
  ...(typeof cur === 'string' && cur && cur !== 'MUST_NOT_BE_PUBLIC_USER' && !names.includes(cur) ? [[cur, `${cur} (does not exist)`] as [string, string]] : [])], help);
41
42
 
42
43
  const crumbs = (a: { id: number; name: string }, last?: string): [string, string?][] => [
43
- ['App Builder', BASE], [a.name, `${BASE}/apps/${a.id}`], ['Shared Components', `${BASE}/apps/${a.id}/shared`],
44
- ...(last ? [['AI Agents', `${BASE}/apps/${a.id}/ai-agents`], [last]] as [string, string?][] : [['AI Agents']] as [string, string?][]),
44
+ [tx('agt.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], [tx('agt.shared_components'), `${BASE}/apps/${a.id}/shared`],
45
+ ...(last ? [[tx('agt.ai_agents'), `${BASE}/apps/${a.id}/ai-agents`], [last]] as [string, string?][] : [[tx('agt.ai_agents')]] as [string, string?][]),
45
46
  ];
46
47
 
47
48
  // ---------------------------------------------------------------- forms
@@ -51,7 +52,7 @@ async function agentForm(a: any, s: Session, x: Partial<AiAgent>, original: stri
51
52
  `select s.name, s.provider, s.model from meta.ai_service s join meta.app_ai_service x on x.service_id = s.id and x.app_id = $1 where s.enabled order by s.name`, [a.id])).rows;
52
53
  const tools = (a.ai_tools ?? []) as AiTool[];
53
54
  const chosen = new Set(x.tools ?? []);
54
- const svc: [string, string][] = [['', '- choose -'], ...services.map((v): [string, string] => [v.name, `${v.name} (${providerLabel(v.provider)}, ${v.model})`])];
55
+ const svc: [string, string][] = [['', tx('agt.choose')], ...services.map((v): [string, string] => [v.name, `${v.name} (${providerLabel(v.provider)}, ${v.model})`])];
55
56
  if (x.service && !services.some((v) => v.name === x.service)) svc.push([x.service, `${x.service} (not available to this application)`]);
56
57
  const usedBy = original
57
58
  ? (await owner.query<{ page_no: number; title: string | null; page_id: number; id: number }>(
@@ -59,31 +60,31 @@ async function agentForm(a: any, s: Session, x: Partial<AiAgent>, original: stri
59
60
  where p.app_id = $1 and r.type = 'ai_assistant' and upper(r.config->>'agent') = $2 order by p.page_no, r.seq`, [a.id, original])).rows
60
61
  : [];
61
62
  return html`${problems.length ? html`<div class="alert alert-error" role="alert">${problems.join(' ')}</div>` : ''}
62
- ${services.length ? '' : html`<p class="muted">This application may not use any AI service yet: an administrator adds one under Workspace utilities → AI services and allows this application to use it.</p>`}
63
+ ${services.length ? '' : html`<p class="muted">${tx('agt.this_application_may_not_use')}</p>`}
63
64
  <form method="post" action="${BASE}/apps/${a.id}/ai-agents/agent">${csrf(s)}<input type="hidden" name="original" value="${original}">
64
- <fieldset class="prop-group"><legend>Agent</legend><div class="form-grid">
65
- ${input('name', 'Name', x.name ?? '', { required: true, placeholder: 'e.g. SALES_AGENT', help: 'Upper case letters, digits and _. AI assistant regions name the agent.' })}
66
- ${input('description', 'Description', x.description ?? '')}
67
- ${select('service', 'AI service', x.service ?? '', svc, 'The conversation, the tools\' results and the system prompt go to this service\'s provider.')}
68
- ${select('effort', 'Reasoning effort', x.effort ?? '', [['', 'The service\'s'], ...EFFORTS.map((e): [string, string] => [e, e])],
69
- 'Claude: the output effort (thinking depth). OpenAI, Gemini and OpenAI-compatible: reasoning_effort (leave empty for models without reasoning).')}
70
- ${input('max_steps', 'Maximum steps', x.max_steps ?? '', { type: 'number', placeholder: '5', help: 'Rounds of tool calls per question (1 – 20); then the model must answer.' })}
71
- ${input('max_tokens', 'Maximum output tokens', x.max_tokens ?? '', { type: 'number', placeholder: 'the service\'s', help: 'Per call to the model (never above the service\'s maximum).' })}
72
- ${authzSelect(await schemes(a.id), x.authz, 'Only users who pass it may use the agent (regions show a message to the others).')}
73
- ${area('system', 'System prompt', x.system ?? '', 6, 'Who the agent is, what it may answer and when to use which tool. &ITEM. is replaced by the item\'s value as delimited data.')}
65
+ <fieldset class="prop-group"><legend>${tx('agt.agent')}</legend><div class="form-grid">
66
+ ${input('name', tx('agt.name'), x.name ?? '', { required: true, placeholder: tx('agt.e_g_sales_agent'), help: tx('agt.upper_case_letters_digits_and') })}
67
+ ${input('description', tx('agt.description'), x.description ?? '')}
68
+ ${select('service', tx('agt.ai_service'), x.service ?? '', svc, tx('agt.the_conversation_the_tools_results'))}
69
+ ${select('effort', tx('agt.reasoning_effort'), x.effort ?? '', [['', tx('agt.the_service_s_2')], ...EFFORTS.map((e): [string, string] => [e, e])],
70
+ tx('agt.effort_help'))}
71
+ ${input('max_steps', tx('agt.maximum_steps'), x.max_steps ?? '', { type: 'number', placeholder: '5', help: tx('agt.rounds_of_tool_calls_per') })}
72
+ ${input('max_tokens', tx('agt.maximum_output_tokens'), x.max_tokens ?? '', { type: 'number', placeholder: tx('agt.the_service_s'), help: tx('agt.per_call_to_the_model') })}
73
+ ${authzSelect(await schemes(a.id), x.authz, tx('agt.only_users_who_pass_it'))}
74
+ ${area('system', tx('agt.system_prompt'), x.system ?? '', 6, tx('agt.system_help'))}
74
75
  </div></fieldset>
75
- <fieldset class="prop-group"><legend>Tools</legend>
76
+ <fieldset class="prop-group"><legend>${tx('agt.tools')}</legend>
76
77
  ${tools.length
77
- ? html`<p class="muted u-mt0">The agent acts only through the tools you tick.</p>${tools.map((t) => html`<div class="field"><label class="check"><input type="checkbox" name="tools" value="${t.name}"${chosen.has(t.name) ? raw(' checked') : ''}> <b>${t.name}</b> <span class="muted">(${t.type}) ${t.description}</span></label></div>`)}`
78
- : html`<p class="muted u-mt0">No AI tools yet: <a href="${BASE}/apps/${a.id}/ai-agents/tool">add one</a>.</p>`}
78
+ ? html`<p class="muted u-mt0">${tx('agt.the_agent_acts_only_through')}</p>${tools.map((t) => html`<div class="field"><label class="check"><input type="checkbox" name="tools" value="${t.name}"${chosen.has(t.name) ? raw(' checked') : ''}> <b>${t.name}</b> <span class="muted">(${t.type}) ${t.description}</span></label></div>`)}`
79
+ : html`<p class="muted u-mt0">${tx.rich('agt.no_tools', { link: html`<a href="${BASE}/apps/${a.id}/ai-agents/tool">${tx('agt.add_one')}</a>` })}</p>`}
79
80
  </fieldset>
80
- <div class="buttons"><button class="btn btn-hot">${original ? 'Save' : 'Add agent'}</button></div>
81
+ <div class="buttons"><button class="btn btn-hot">${original ? tx('agt.save') : tx('agt.add_agent')}</button></div>
81
82
  </form>
82
- ${original ? html`<h3>Used by</h3>${usedBy.length
83
+ ${original ? html`<h3>${tx('agt.used_by')}</h3>${usedBy.length
83
84
  ? html`<ul>${usedBy.map((r) => html`<li><a href="${BASE}/pages/${r.page_id}?c=region-${r.id}">Page ${r.page_no}: ${r.title ?? 'AI assistant'}</a></li>`)}</ul>`
84
- : html`<p class="muted">No AI assistant region uses this agent yet (Page Designer → an AI assistant region → AI agent).</p>`}
85
+ : html`<p class="muted">${tx('agt.no_ai_assistant_region_uses')}</p>`}
85
86
  <form method="post" action="${BASE}/apps/${a.id}/ai-agents/agent/delete" class="danger-zone">${csrf(s)}<input type="hidden" name="name" value="${original}">
86
- <button class="btn btn-danger" data-confirm="Delete agent ${original}? Regions that use it show that the assistant is not set up.">Delete agent</button></form>` : ''}`;
87
+ <button class="btn btn-danger" data-confirm="${tx('agt.delete_agent_confirm', { name: original })}">${tx('agt.delete_agent')}</button></form>` : ''}`;
87
88
  }
88
89
 
89
90
  async function toolForm(a: any, s: Session, x: Partial<AiTool> & { parameters_text?: string }, original: string, problems: string[]): Promise<Raw> {
@@ -93,28 +94,28 @@ async function toolForm(a: any, s: Session, x: Partial<AiTool> & { parameters_te
93
94
  const usedBy = original ? ((a.ai_agents ?? []) as AiAgent[]).filter((g) => (g.tools ?? []).includes(original)).map((g) => g.name) : [];
94
95
  return html`${problems.length ? html`<div class="alert alert-error" role="alert">${problems.join(' ')}</div>` : ''}
95
96
  <form method="post" action="${BASE}/apps/${a.id}/ai-agents/tool">${csrf(s)}<input type="hidden" name="original" value="${original}">
96
- <fieldset class="prop-group"><legend>Tool</legend><div class="form-grid">
97
- ${input('name', 'Name', x.name ?? '', { required: true, placeholder: 'e.g. top_earners', help: 'Letters, digits and _: the model calls the tool by this name.' })}
98
- ${select('type', 'Type', x.type ?? 'sql', TOOL_TYPES, 'What the tool does; each type uses the fields below that name it.')}
99
- ${area('description', 'Description', x.description ?? '', 2, 'Tells the model what the tool does and when to use it.')}
100
- ${area('sql', 'SQL query (SQL)', x.sql ?? '', 6, 'One SELECT with the parameters as :NAME bind values (and :APP_USER, items). Runs as the application\'s database role (grants and row level security apply) in a read-only transaction.', true)}
101
- ${input('function', 'Function (PL/pgSQL)', x.function ?? '', { placeholder: 'e.g. sales.give_discount', help: 'schema.name, called as the application\'s role with named arguments: each parameter name in lower case is an argument name (P_ORDER → p_order). It may change data.' })}
102
- ${select('source', 'REST data source (REST)', x.source ?? '', [['', '-'], ...sources.map((n): [string, string] => [n, n])], 'The parameters become the source\'s parameters.')}
103
- ${input('action', 'Action (JavaScript)', x.action ?? '', { placeholder: 'e.g. highlightOrder', help: 'After the answer the browser calls the function registered with pgkiln.actions.register(name) with {value: the arguments}, and the dynamic actions of the custom event of this name.' })}
104
- ${input('pages', 'Pages (Navigate)', (x.pages ?? []).join(', '), { placeholder: 'e.g. 3, 7', help: 'The page numbers the agent may link to. pgkiln offers only those the user may open and builds the URL (item values with their checksum).' })}
105
- ${select('search_config', 'Search configuration (Retrieve)', x.search_config ?? '', [['', '-'], ...configs.map((c): [string, string] => [c.name, `${c.name} (${c.search_type})`]),
97
+ <fieldset class="prop-group"><legend>${tx('agt.tool')}</legend><div class="form-grid">
98
+ ${input('name', tx('agt.name'), x.name ?? '', { required: true, placeholder: tx('agt.e_g_top_earners'), help: tx('agt.letters_digits_and_the_model') })}
99
+ ${select('type', tx('agt.type'), x.type ?? 'sql', TOOL_TYPES(), tx('agt.what_the_tool_does_each'))}
100
+ ${area('description', tx('agt.description'), x.description ?? '', 2, tx('agt.tells_the_model_what_the'))}
101
+ ${area('sql', tx('agt.sql_query_sql'), x.sql ?? '', 6, tx('agt.sql_help'), true)}
102
+ ${input('function', tx('agt.function_pl_pgsql'), x.function ?? '', { placeholder: tx('agt.e_g_sales_give_discount'), help: tx('agt.schema_name_called_as_the') })}
103
+ ${select('source', tx('agt.rest_data_source_rest'), x.source ?? '', [['', '-'], ...sources.map((n): [string, string] => [n, n])], tx('agt.the_parameters_become_the_source'))}
104
+ ${input('action', tx('agt.action_javascript'), x.action ?? '', { placeholder: tx('agt.e_g_highlightorder'), help: tx('agt.after_the_answer_the_browser') })}
105
+ ${input('pages', tx('agt.pages_navigate'), (x.pages ?? []).join(', '), { placeholder: 'e.g. 3, 7', help: tx('agt.the_page_numbers_the_agent') })}
106
+ ${select('search_config', tx('agt.search_configuration_retrieve'), x.search_config ?? '', [['', '-'], ...configs.map((c): [string, string] => [c.name, `${c.name} (${c.search_type})`]),
106
107
  ...(x.search_config && !configs.some((c) => c.name === x.search_config) ? [[x.search_config, `${x.search_config} (does not exist)`] as [string, string]] : [])],
107
108
  'The model gives a text; pgkiln runs this search configuration (best: vector or hybrid) as the application\'s role, so row level security applies, and returns the best rows\' title, text and score.')}
108
- ${area('parameters', 'Parameters (JSON; not for Navigate or Retrieve)', params, 6, '{"DNAME": {"type": "string", "description": "A department name"}, "LIMIT": {"type": "integer", "optional": true}}. Types: string, integer, number, boolean, date; "enum" for a list of texts.', true)}
109
- ${input('max_rows', 'Maximum rows', x.max_rows ?? '', { type: 'number', placeholder: '50', help: 'Rows the model gets back (1 – 500).' })}
110
- ${input('timeout_s', 'Time limit (seconds)', x.timeout_s ?? '', { type: 'number', placeholder: '15', help: 'SQL and function tools (1 – 60).' })}
111
- ${authzSelect(await schemes(a.id), x.authz, 'Only users who pass it are offered the tool.')}
109
+ ${area('parameters', tx('agt.parameters_json_not_for_navigate'), params, 6, '{"DNAME": {"type": "string", "description": "A department name"}, "LIMIT": {"type": "integer", "optional": true}}. Types: string, integer, number, boolean, date; "enum" for a list of texts.', true)}
110
+ ${input('max_rows', tx('agt.maximum_rows'), x.max_rows ?? '', { type: 'number', placeholder: '50', help: tx('agt.rows_the_model_gets_back') })}
111
+ ${input('timeout_s', tx('agt.time_limit_seconds'), x.timeout_s ?? '', { type: 'number', placeholder: '15', help: tx('agt.sql_and_function_tools_1') })}
112
+ ${authzSelect(await schemes(a.id), x.authz, tx('agt.only_users_who_pass_it_2'))}
112
113
  </div></fieldset>
113
- <div class="buttons"><button class="btn btn-hot">${original ? 'Save' : 'Add tool'}</button></div>
114
+ <div class="buttons"><button class="btn btn-hot">${original ? tx('agt.save') : tx('agt.add_tool')}</button></div>
114
115
  </form>
115
- ${original ? html`<h3>Used by</h3>${usedBy.length ? html`<p>${usedBy.join(', ')}</p>` : html`<p class="muted">No agent uses this tool yet.</p>`}
116
+ ${original ? html`<h3>${tx('agt.used_by')}</h3>${usedBy.length ? html`<p>${usedBy.join(', ')}</p>` : html`<p class="muted">${tx('agt.no_agent_uses_this_tool')}</p>`}
116
117
  <form method="post" action="${BASE}/apps/${a.id}/ai-agents/tool/delete" class="danger-zone">${csrf(s)}<input type="hidden" name="name" value="${original}">
117
- <button class="btn btn-danger" data-confirm="Delete tool ${original}?">Delete tool</button></form>` : ''}`;
118
+ <button class="btn btn-danger" data-confirm="${tx('agt.delete_tool_confirm', { name: original })}">${tx('agt.delete_tool')}</button></form>` : ''}`;
118
119
  }
119
120
 
120
121
  // ---------------------------------------------------------------- reading the forms
@@ -123,7 +124,7 @@ const intOrUndef = (v: string, what: string, errors: string[]) => {
123
124
  const t = v.trim();
124
125
  if (!t) return undefined;
125
126
  const n = Number(t);
126
- if (!Number.isInteger(n)) errors.push(`${what} is a whole number.`);
127
+ if (!Number.isInteger(n)) errors.push(tx('agt.whole_number', { what }));
127
128
  return n;
128
129
  };
129
130
 
@@ -139,9 +140,9 @@ function agentFrom(b: Body, errors: string[]): AiAgent {
139
140
  if (effort) x.effort = effort as AiAgent['effort'];
140
141
  const tools = all(b.tools).map((t) => t.trim()).filter(Boolean);
141
142
  if (tools.length) x.tools = [...new Set(tools)];
142
- const steps = intOrUndef(one(b.max_steps), 'Maximum steps', errors);
143
+ const steps = intOrUndef(one(b.max_steps), tx('agt.f_steps'), errors);
143
144
  if (steps !== undefined) x.max_steps = steps;
144
- const tokens = intOrUndef(one(b.max_tokens), 'Maximum output tokens', errors);
145
+ const tokens = intOrUndef(one(b.max_tokens), tx('agt.f_tokens'), errors);
145
146
  if (tokens !== undefined) x.max_tokens = tokens;
146
147
  const authz = one(b.authz).trim();
147
148
  if (authz) x.authz = authz;
@@ -162,16 +163,16 @@ function toolFrom(b: Body, errors: string[]): AiTool & { parameters_text?: strin
162
163
  try {
163
164
  x.parameters = JSON.parse(params);
164
165
  } catch {
165
- errors.push('Parameters: not valid JSON.');
166
+ errors.push(tx('agt.parameters_not_valid_json'));
166
167
  x.parameters_text = params;
167
168
  }
168
169
  }
169
170
  if (type === 'sql' || type === 'function' || type === 'rest' || type === 'retrieve') {
170
- const rows = intOrUndef(one(b.max_rows), 'Maximum rows', errors);
171
+ const rows = intOrUndef(one(b.max_rows), tx('agt.f_rows'), errors);
171
172
  if (rows !== undefined) x.max_rows = rows;
172
173
  }
173
174
  if (type === 'sql' || type === 'function' || type === 'retrieve') {
174
- const t = intOrUndef(one(b.timeout_s), 'Time limit', errors);
175
+ const t = intOrUndef(one(b.timeout_s), tx('agt.f_timeout'), errors);
175
176
  if (t !== undefined) x.timeout_s = t;
176
177
  }
177
178
  const authz = one(b.authz).trim();
@@ -197,15 +198,15 @@ async function update(appId: number, change: (agents: AiAgent[], tools: AiTool[]
197
198
  /** The "Tool calls" region of Activity → AI usage: the latest calls with their arguments. */
198
199
  export async function toolCallTable(appId: number): Promise<Raw> {
199
200
  const rows = (await owner.query('select * from meta.ai_tool_call where app_id = $1 order by id desc limit 100', [appId])).rows;
200
- return region('Tool calls', html`<p class="muted u-mt0">Every tool call of the application's AI assistants and agents, with the arguments the model gave (kept 30 days).</p>
201
+ return region(tx('agt.tool_calls'), html`<p class="muted u-mt0">${tx('agt.every_tool_call_of_the')}</p>
201
202
  <div class="table-wrap"><table class="report report-reflow">
202
- <thead><tr><th>When (UTC)</th><th class="num">Page</th><th>User</th><th>Agent</th><th>Tool</th><th>Arguments</th><th class="num">ms</th><th>Status</th></tr></thead>
203
+ <thead><tr><th>${tx('agt.when_utc')}</th><th class="num">${tx('agt.page')}</th><th>${tx('agt.user')}</th><th>${tx('agt.agent')}</th><th>${tx('agt.tool')}</th><th>${tx('agt.arguments')}</th><th class="num">ms</th><th>${tx('agt.status')}</th></tr></thead>
203
204
  <tbody>${rows.length ? rows.map((r) => html`<tr>
204
- <td data-label="When">${when(r.at)}</td><td class="num" data-label="Page">${r.page_no ?? ''}</td><td data-label="User">${r.username ?? ''}</td>
205
- <td data-label="Agent">${r.agent ?? html`<span class="muted">region</span>`}</td><td data-label="Tool">${r.tool} <span class="muted">${r.type}</span></td>
206
- <td data-label="Arguments"><code class="ai-args">${JSON.stringify(r.arguments)}</code></td><td class="num" data-label="ms">${r.duration_ms}</td>
207
- <td data-label="Status"><span class="tag${r.status === 'ok' ? ' tag-ok' : ' tag-error'}">${r.status}</span>${r.message ? html` <span class="muted">${r.message}</span>` : ''}</td></tr>`)
208
- : html`<tr><td colspan="8" class="empty">No tool calls yet.</td></tr>`}</tbody></table></div>`);
205
+ <td data-label="${tx('agt.when')}">${when(r.at)}</td><td class="num" data-label="${tx('agt.page')}">${r.page_no ?? ''}</td><td data-label="${tx('agt.user')}">${r.username ?? ''}</td>
206
+ <td data-label="${tx('agt.agent')}">${r.agent ?? html`<span class="muted">region</span>`}</td><td data-label="${tx('agt.tool')}">${r.tool} <span class="muted">${r.type}</span></td>
207
+ <td data-label="${tx('agt.arguments')}"><code class="ai-args">${JSON.stringify(r.arguments)}</code></td><td class="num" data-label="ms">${r.duration_ms}</td>
208
+ <td data-label="${tx('agt.status')}"><span class="tag${r.status === 'ok' ? ' tag-ok' : ' tag-error'}">${r.status}</span>${r.message ? html` <span class="muted">${r.message}</span>` : ''}</td></tr>`)
209
+ : html`<tr><td colspan="8" class="empty">${tx('agt.no_tool_calls_yet')}</td></tr>`}</tbody></table></div>`);
209
210
  }
210
211
 
211
212
  // ---------------------------------------------------------------- routes
@@ -230,22 +231,22 @@ export async function aiAgentRoutes(app: FastifyInstance) {
230
231
  const tools = (a.ai_tools ?? []) as AiTool[];
231
232
  const problems = definitionProblems(agents, tools);
232
233
  const main = html`${appHeader(a, 'shared', s)}
233
- <div class="title-row"><h1>AI Agents</h1></div>
234
- <p class="muted u-mt0">An agent answers in an AI assistant region and acts only through the tools you give it: SQL queries (read-only, as the application's database role, so row level security applies), PL/pgSQL functions, REST data sources, JavaScript actions in the browser, and links to pages (pgkiln builds the URL and its checksum, only for pages the user may open). A Retrieve tool gives the agent the rows of a search configuration that match a text by meaning (RAG), again as the application's role. What users type and the tools' results go to the agent's AI service. <a href="${BASE}/apps/${a.id}/ai">AI usage</a> lists every tool call.</p>
234
+ <div class="title-row"><h1>${tx('agt.ai_agents')}</h1></div>
235
+ <p class="muted u-mt0">${tx.rich('agt.intro', { usage: html`<a href="${BASE}/apps/${a.id}/ai">${tx('agt.ai_usage')}</a>` })}</p>
235
236
  ${problems.length ? html`<div class="alert alert-error" role="alert">${problems.join(' ')}</div>` : ''}
236
237
  <div class="columns">
237
- ${region('Agents', html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th>Agent</th><th>AI service</th><th>Tools</th><th>Authorization</th></tr></thead>
238
- <tbody>${agents.length ? agents.map((g) => html`<tr><td data-label="Agent"><a href="${BASE}/apps/${a.id}/ai-agents/agent?name=${encodeURIComponent(g.name)}">${g.name}</a>${g.description ? html` <span class="muted">${g.description}</span>` : ''}</td>
239
- <td data-label="AI service">${g.service}${g.effort ? html` <span class="muted">(${g.effort})</span>` : ''}</td><td data-label="Tools">${(g.tools ?? []).join(', ')}</td><td data-label="Authorization">${g.authz ?? ''}</td></tr>`)
240
- : html`<tr><td colspan="4" class="empty">No AI agents yet.</td></tr>`}</tbody></table></div>
241
- <p><a class="btn" href="${BASE}/apps/${a.id}/ai-agents/agent">${icon('plus')} Add agent</a></p>`)}
242
- ${region('Tools', html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th>Tool</th><th>Type</th><th>Authorization</th></tr></thead>
243
- <tbody>${tools.length ? tools.map((t) => html`<tr><td data-label="Tool"><a href="${BASE}/apps/${a.id}/ai-agents/tool?name=${encodeURIComponent(t.name)}">${t.name}</a> <span class="muted">${t.description}</span></td>
244
- <td data-label="Type">${t.type}</td><td data-label="Authorization">${t.authz ?? ''}</td></tr>`)
245
- : html`<tr><td colspan="3" class="empty">No AI tools yet.</td></tr>`}</tbody></table></div>
246
- <p><a class="btn" href="${BASE}/apps/${a.id}/ai-agents/tool">${icon('plus')} Add tool</a></p>`)}
238
+ ${region(tx('agt.agents'), html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th>${tx('agt.agent')}</th><th>${tx('agt.ai_service')}</th><th>${tx('agt.tools')}</th><th>${tx('agt.authorization')}</th></tr></thead>
239
+ <tbody>${agents.length ? agents.map((g) => html`<tr><td data-label="${tx('agt.agent')}"><a href="${BASE}/apps/${a.id}/ai-agents/agent?name=${encodeURIComponent(g.name)}">${g.name}</a>${g.description ? html` <span class="muted">${g.description}</span>` : ''}</td>
240
+ <td data-label="${tx('agt.ai_service')}">${g.service}${g.effort ? html` <span class="muted">(${g.effort})</span>` : ''}</td><td data-label="${tx('agt.tools')}">${(g.tools ?? []).join(', ')}</td><td data-label="${tx('agt.authorization')}">${g.authz ?? ''}</td></tr>`)
241
+ : html`<tr><td colspan="4" class="empty">${tx('agt.no_ai_agents_yet')}</td></tr>`}</tbody></table></div>
242
+ <p><a class="btn" href="${BASE}/apps/${a.id}/ai-agents/agent">${icon('plus')} ${tx('agt.add_agent')}</a></p>`)}
243
+ ${region(tx('agt.tools'), html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th>${tx('agt.tool')}</th><th>${tx('agt.type')}</th><th>${tx('agt.authorization')}</th></tr></thead>
244
+ <tbody>${tools.length ? tools.map((t) => html`<tr><td data-label="${tx('agt.tool')}"><a href="${BASE}/apps/${a.id}/ai-agents/tool?name=${encodeURIComponent(t.name)}">${t.name}</a> <span class="muted">${t.description}</span></td>
245
+ <td data-label="${tx('agt.type')}">${t.type}</td><td data-label="${tx('agt.authorization')}">${t.authz ?? ''}</td></tr>`)
246
+ : html`<tr><td colspan="3" class="empty">${tx('agt.no_ai_tools_yet')}</td></tr>`}</tbody></table></div>
247
+ <p><a class="btn" href="${BASE}/apps/${a.id}/ai-agents/tool">${icon('plus')} ${tx('agt.add_tool')}</a></p>`)}
247
248
  </div>`;
248
- return send(reply, s, shell(s, 'AI Agents', crumbs(a), main));
249
+ return send(reply, s, shell(s, tx('agt.ai_agents'), crumbs(a), main));
249
250
  });
250
251
 
251
252
  app.get(`${BASE}/apps/:id(^\\d+$)/ai-agents/agent`, async (req: Req, reply) => {
@@ -256,7 +257,7 @@ export async function aiAgentRoutes(app: FastifyInstance) {
256
257
  const x = name ? ((a.ai_agents ?? []) as AiAgent[]).find((g) => g.name === name) : {};
257
258
  if (!x) return reply.code(404).send('Not found');
258
259
  const title = name || 'New agent';
259
- const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${title}</h1></div>${region('AI agent', await agentForm(a, s, x, name, []))}`;
260
+ const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${title}</h1></div>${region(tx('agt.ai_agent'), await agentForm(a, s, x, name, []))}`;
260
261
  return send(reply, s, shell(s, title, crumbs(a, title), main));
261
262
  });
262
263
 
@@ -271,21 +272,21 @@ export async function aiAgentRoutes(app: FastifyInstance) {
271
272
  const problems = errors.length ? errors : await update(a.id, (agents, tools) => {
272
273
  const out = agentProblems(x, new Set(tools.map((t) => t.name)));
273
274
  const at = original ? agents.findIndex((g) => g.name === original) : -1;
274
- if (original && at < 0) out.push(`Agent ${original} no longer exists.`);
275
- if (agents.some((g, i) => g.name === x.name && i !== at)) out.push(`An agent ${x.name} exists already.`);
275
+ if (original && at < 0) out.push(tx('agt.agent_gone', { name: original }));
276
+ if (agents.some((g, i) => g.name === x.name && i !== at)) out.push(tx('agt.agent_exists', { name: x.name }));
276
277
  if (out.length) return out;
277
278
  if (at >= 0) agents[at] = x;
278
279
  else agents.push(x);
279
280
  return [];
280
281
  });
281
282
  if (problems.length) {
282
- const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${original || 'New agent'}</h1></div>${region('AI agent', await agentForm(a, s, x, original, problems))}`;
283
+ const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${original || 'New agent'}</h1></div>${region(tx('agt.ai_agent'), await agentForm(a, s, x, original, problems))}`;
283
284
  return send(reply.code(422), s, shell(s, original || 'New agent', crumbs(a, original || 'New agent'), main));
284
285
  }
285
286
  if (original && original !== x.name)
286
287
  await owner.query(`update meta.region r set config = jsonb_set(r.config, '{agent}', to_jsonb($3::text)) from meta.page p
287
288
  where p.id = r.page_id and p.app_id = $1 and r.type = 'ai_assistant' and upper(r.config->>'agent') = $2`, [a.id, original, x.name]);
288
- flash(s, 'AI agent saved.');
289
+ flash(s, tx('agt.ai_agent_saved'));
289
290
  return back(reply, s, `${BASE}/apps/${a.id}/ai-agents/agent?name=${encodeURIComponent(x.name)}`);
290
291
  });
291
292
 
@@ -311,7 +312,7 @@ export async function aiAgentRoutes(app: FastifyInstance) {
311
312
  const x = name ? ((a.ai_tools ?? []) as AiTool[]).find((t) => t.name === name) : {};
312
313
  if (!x) return reply.code(404).send('Not found');
313
314
  const title = name || 'New tool';
314
- const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${title}</h1></div>${region('AI tool', await toolForm(a, s, x, name, []))}`;
315
+ const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${title}</h1></div>${region(tx('agt.ai_tool'), await toolForm(a, s, x, name, []))}`;
315
316
  return send(reply, s, shell(s, title, crumbs(a, title), main));
316
317
  });
317
318
 
@@ -327,15 +328,15 @@ export async function aiAgentRoutes(app: FastifyInstance) {
327
328
  const problems = errors.length ? errors : await update(a.id, async (agents, tools) => {
328
329
  const out = aiToolProblems(tool);
329
330
  if (tool.type === 'rest' && tool.source && !(await owner.one('select 1 from meta.rest_source where app_id = $1 and name = $2', [a.id, tool.source])))
330
- out.push(`There is no REST data source ${tool.source}.`);
331
+ out.push(tx('agt.no_rest_source', { name: tool.source }));
331
332
  if (tool.type === 'retrieve' && tool.search_config && !(await owner.one(`select 1 from meta.search_config where app_id = $1 and name = $2 and source_type <> 'list'`, [a.id, tool.search_config])))
332
- out.push(`There is no search configuration ${tool.search_config} with a table or SQL source.`);
333
+ out.push(tx('agt.no_search_config', { name: tool.search_config }));
333
334
  if (tool.type === 'navigate') {
334
335
  const known = new Set((await owner.query<{ page_no: number }>('select page_no from meta.page where app_id = $1', [a.id])).rows.map((p) => p.page_no));
335
- for (const p of tool.pages ?? []) if (!known.has(p)) out.push(`There is no page ${p}.`);
336
+ for (const p of tool.pages ?? []) if (!known.has(p)) out.push(tx('agt.no_page', { n: p }));
336
337
  }
337
338
  const at = original ? tools.findIndex((t) => t.name === original) : -1;
338
- if (original && at < 0) out.push(`Tool ${original} no longer exists.`);
339
+ if (original && at < 0) out.push(tx('agt.tool_gone', { name: original }));
339
340
  if (tools.some((t, i) => t.name === tool.name && i !== at)) out.push(`A tool ${tool.name} exists already.`);
340
341
  if (out.length) return out;
341
342
  if (at >= 0) tools[at] = tool;
@@ -345,10 +346,10 @@ export async function aiAgentRoutes(app: FastifyInstance) {
345
346
  return [];
346
347
  });
347
348
  if (problems.length) {
348
- const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${original || 'New tool'}</h1></div>${region('AI tool', await toolForm(a, s, x, original, problems))}`;
349
+ const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${original || 'New tool'}</h1></div>${region(tx('agt.ai_tool'), await toolForm(a, s, x, original, problems))}`;
349
350
  return send(reply.code(422), s, shell(s, original || 'New tool', crumbs(a, original || 'New tool'), main));
350
351
  }
351
- flash(s, 'AI tool saved.');
352
+ flash(s, tx('agt.ai_tool_saved'));
352
353
  return back(reply, s, `${BASE}/apps/${a.id}/ai-agents/tool?name=${encodeURIComponent(tool.name)}`);
353
354
  });
354
355
 
@@ -359,7 +360,7 @@ export async function aiAgentRoutes(app: FastifyInstance) {
359
360
  const name = one((req.body as Body)?.name);
360
361
  const problems = await update(a.id, (agents, tools) => {
361
362
  const users = agents.filter((g) => (g.tools ?? []).includes(name)).map((g) => g.name);
362
- if (users.length) return [`Tool ${name} is used by ${users.join(', ')}: take it out of those agents first.`];
363
+ if (users.length) return [tx('agt.tool_used', { name, agents: users.join(', ') })];
363
364
  const at = tools.findIndex((t) => t.name === name);
364
365
  if (at >= 0) tools.splice(at, 1);
365
366
  return [];