toga-ai 1.0.727 → 1.0.729
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md +45 -3
- package/knowledge/2.0/apps/talos/features/aegra-api.md +35 -2
- package/knowledge/2.0/apps/talos/features/chat-frontend.md +15 -4
- package/knowledge/2.0/apps/toga-blox/INDEX.md +1 -1
- package/knowledge/2.0/apps/toga-blox/features/talos-assistant.md +86 -22
- package/knowledge/2.0/apps/toga-blox/workflows/local-link-into-a-consumer-app.md +35 -1
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +2 -2
- package/knowledge/2.0/apps/toga25-supply/architecture.md +13 -2
- package/knowledge/2.0/apps/toga25-supply/features/talos-integration.md +183 -104
- package/knowledge/2.0/standards/frontend.md +22 -1
- package/knowledge/clients/compass-canada/profile.md +8 -0
- package/knowledge/clients/compass-usa/features/item-catalogs-and-duplicate-items.md +39 -3
- package/knowledge/clients/nychh/profile.md +1 -0
- package/knowledge/clients/quad/profile.md +12 -0
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-01
|
|
10
10
|
owners: ["bala"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -14,6 +14,7 @@ files:
|
|
|
14
14
|
- toga2-supply/src/api/toga.ts
|
|
15
15
|
related:
|
|
16
16
|
- ../../../clients/compass-usa/features/item-fulfillment-tracking-tableview.md
|
|
17
|
+
- ../../../clients/compass-usa/features/item-catalogs-and-duplicate-items.md
|
|
17
18
|
- tableview-field-metadata.md
|
|
18
19
|
---
|
|
19
20
|
|
|
@@ -36,8 +37,9 @@ view with a migration and no application code.
|
|
|
36
37
|
- **Consumption** — `toga2-supply/src/api/toga.ts` (~line 943-951): pushes the `apiWhereClause`
|
|
37
38
|
literal string into `options.where.and`, and (~line 631-641) builds `options.join` / `ojoin`
|
|
38
39
|
from the view's `TableViewJoins`.
|
|
39
|
-
- **Parsing** — `api2/Component/Api/V2/V2.php::parseOptionsWhere` (~line
|
|
40
|
-
|
|
40
|
+
- **Parsing** — `api2/Component/Api/V2/V2.php::parseOptionsWhere` (~line 8200-8390, re-located
|
|
41
|
+
2026-09-01; it was ~7574 when this doc was first written, so search for the function name rather
|
|
42
|
+
than trusting the line number): turns the clause string into SQL.
|
|
41
43
|
|
|
42
44
|
## How it works
|
|
43
45
|
|
|
@@ -68,6 +70,22 @@ The field slot accepts a **raw SQL expression**, not just a plain column — e.g
|
|
|
68
70
|
exclusion on the Compass fulfillment view): `(IFNULL(Items.assetTypeId,0):ne:<FEE id>)` — the
|
|
69
71
|
`IFNULL(...,0)` keeps rows with a NULL FK from being silently dropped.
|
|
70
72
|
|
|
73
|
+
### A single-condition clause is wrapped in parens too
|
|
74
|
+
|
|
75
|
+
`parseOptionsWhere` **strips one leading `(` and one trailing `)` from the whole clause before it
|
|
76
|
+
scans for top-depth commas** (V2.php ~line 8222-8229). Those outer parens are the group wrapper,
|
|
77
|
+
not part of the first condition, so the house form for a one-condition filter is:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
(Items.catalogId:eq:1)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The wrapper comes off first, leaving `Items.catalogId:eq:1`, which is then read as a plain
|
|
84
|
+
condition — it is never mistaken for a nested group needing recursion. Write the parens; they match
|
|
85
|
+
the existing Compass convention and the stored clauses stay uniform whether they hold one condition
|
|
86
|
+
or five. (The "expression field must not start with `(`" gotcha below is about the *field slot* of a
|
|
87
|
+
condition **after** the wrapper is removed — the two are not in conflict.)
|
|
88
|
+
|
|
71
89
|
### Join alias rule (which table-qualified name to use)
|
|
72
90
|
|
|
73
91
|
In `_underscore/Model/Client/TableView.php` (~line 89-96), the **first** occurrence of a joined
|
|
@@ -77,6 +95,19 @@ referencing `Items.<field>` only binds correctly when the view joins `Items` **e
|
|
|
77
95
|
Before writing a clause, confirm the target table is joined once, or qualify against the correct
|
|
78
96
|
suffixed alias.
|
|
79
97
|
|
|
98
|
+
**A view's base record is not a join, so it never gets a suffix.** Worked example — the shared
|
|
99
|
+
`items` view (`TableViews.slug = 'items'`, `id 9`, `recordId 21` = `_Model_Client_Item`, route
|
|
100
|
+
`items`; the same shape in `Client_Compass`, `Client_CompassCanada` and `Client_Quad`). Its four
|
|
101
|
+
`TableViewJoins` are `joinRecordId` **20** Manufacturers, **33** ItemTypes, **32** AssetTypes and
|
|
102
|
+
**34** ItemCategories, all INNER. `Items` appears only as the base record and is never re-joined,
|
|
103
|
+
so the bare `Items.` qualifier is correct and there is no `_B`/`_C` to worry about. Check this the
|
|
104
|
+
same way on any view before writing a clause: list the view's `TableViewJoins` and count how many
|
|
105
|
+
resolve to the table you want to filter.
|
|
106
|
+
|
|
107
|
+
Two schema notes for anyone writing the migration: `TableViews` has **no `name` or `tableName`
|
|
108
|
+
column** in these client schemas — identify a view by its `slug` (UNIQUE) plus `recordId`, which is
|
|
109
|
+
what makes `WHERE slug = 'items'` a safe, id-agnostic target across tenants.
|
|
110
|
+
|
|
80
111
|
## Data model
|
|
81
112
|
|
|
82
113
|
- `TableViews.apiWhereClause` (TEXT, nullable) — the view-level row filter.
|
|
@@ -91,6 +122,8 @@ None — engine behavior. A specific client's view may carry its own `apiWhereCl
|
|
|
91
122
|
|
|
92
123
|
- **Comma inside a function is safe.** The parser only splits conditions on top-depth commas, so
|
|
93
124
|
a comma inside `IFNULL(x,0)` or `IN(...)` does not break the clause.
|
|
125
|
+
- **A one-condition clause still needs its outer parens** — they are stripped before parsing, so
|
|
126
|
+
`(Field:eq:value)` and `Field:eq:value` behave identically; use the wrapped form for consistency.
|
|
94
127
|
- **An expression field must NOT start with `(`.** A condition that starts with `(` is treated by
|
|
95
128
|
the parser as a nested group and recursed into — so wrap an expression field in a function that
|
|
96
129
|
starts with a letter (e.g. `IFNULL(...)`), never a bare parenthesized expression.
|
|
@@ -102,6 +135,15 @@ None — engine behavior. A specific client's view may carry its own `apiWhereCl
|
|
|
102
135
|
|
|
103
136
|
## Change history
|
|
104
137
|
|
|
138
|
+
- 2026-09-01 — Recorded that the parser **strips one leading `(` and one trailing `)` from the whole
|
|
139
|
+
clause before splitting**, so the wrapped single-condition form `(Items.catalogId:eq:1)` is safe
|
|
140
|
+
and is the house convention (this was previously only documented for the multi-condition form).
|
|
141
|
+
Added a worked alias-verification example on the shared `items` view (id 9, recordId 21, four
|
|
142
|
+
INNER `TableViewJoins` 20/33/32/34, `Items` only as base record → bare `Items.` qualifier), noted
|
|
143
|
+
that `TableViews` carries no `name`/`tableName` column so `slug` is the id-agnostic target, and
|
|
144
|
+
re-pointed `parseOptionsWhere` at its current location (~8200, was ~7574). No code change —
|
|
145
|
+
verified while writing the catalog-1 filter migration for the `items` view in `Client_Compass`,
|
|
146
|
+
`Client_CompassCanada` and `Client_Quad`. (bala)
|
|
105
147
|
- 2026-07-15 — Documented the end-to-end `apiWhereClause` row-filter mechanism (emission →
|
|
106
148
|
consumption → `parseOptionsWhere` grammar), the operator→SQL table, the raw-expression field
|
|
107
149
|
slot, the leading-paren / top-depth-comma parser behavior, and the `_underscore` join-alias
|
|
@@ -6,8 +6,8 @@ project: TOGa IQ
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [akhokhani]
|
|
9
|
+
updated: 2026-09-01
|
|
10
|
+
owners: [akhokhani, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- talos/libs/aegra-api/src/aegra_api/main.py
|
|
13
13
|
- talos/libs/aegra-api/src/aegra_api/settings.py
|
|
@@ -43,6 +43,8 @@ related:
|
|
|
43
43
|
- ../architecture.md
|
|
44
44
|
- mcp-servers.md
|
|
45
45
|
- observability.md
|
|
46
|
+
- chat-frontend.md
|
|
47
|
+
- ../../toga25-supply/features/talos-integration.md
|
|
46
48
|
---
|
|
47
49
|
|
|
48
50
|
## Summary
|
|
@@ -202,5 +204,36 @@ None — uniform across clients. Per-tenant data isolation is enforced by
|
|
|
202
204
|
- **Alembic password chars** — keep the `config.attributes` URL passing pattern
|
|
203
205
|
in `env.py`; configparser interpolation will mangle `%` in passwords.
|
|
204
206
|
|
|
207
|
+
### For browser clients (found 2026-09-01 wiring toga25-supply)
|
|
208
|
+
|
|
209
|
+
- **⚠ CORS headers come back on the OPTIONS PREFLIGHT ONLY — not on the actual response.**
|
|
210
|
+
Verified from both a local dev origin (`http://nychh.dev.sandbox.togasupply:5173`) and a prod
|
|
211
|
+
origin (`https://nychh.togasupply.com`): the preflight returns
|
|
212
|
+
`access-control-allow-origin: *`, but the real `GET` carries **no `access-control-*` header at
|
|
213
|
+
all**. A browser therefore blocks every real response even when the service is perfectly healthy.
|
|
214
|
+
Reported to `akhokhani`; a server-side caller (curl, the old talos proxy route) never sees this.
|
|
215
|
+
- **`AUTH_TYPE=togahub` accepts an api2 JWT issued for a NON-TogaHub client.** Confirmed with a
|
|
216
|
+
**toga25-supply** token whose `aud` was a supply hostname (`https://<tenant>.togasupply.com`) and
|
|
217
|
+
whose `iss`/`sub` were `http://api.beta.togahub.com` — `GET /assistants` succeeded. So a sibling
|
|
218
|
+
app needs **no** token exchange and no `/auth/delegator` handoff; it passes the token it already
|
|
219
|
+
holds. (Do not read `aud` as an audience check — it is not enforced.)
|
|
220
|
+
- **`assistant_id` is per-tenant. Discover it, never pin it in env.**
|
|
221
|
+
`GET /assistants?limit=100&offset=0` returns
|
|
222
|
+
`{ assistants: [{ assistant_id, name, graph_id, metadata: { customer_facing, read_only,
|
|
223
|
+
tenant_slug } }], total }`. Pick on `metadata.customer_facing === true`. Send **no body and no
|
|
224
|
+
`Content-Type`** — `fetch` forbids a body on GET and a JSON content-type only adds a preflight.
|
|
225
|
+
- **An unregistered tenant gets a clean 403 on every call even with a valid token** (control-plane
|
|
226
|
+
DB / `tenant_router` LRU). Provisioning is explicit; there is no auto-create.
|
|
227
|
+
- *Point-in-time note, may be stale:* on **2026-09-01** `https://api.beta.togaiq.com` returned
|
|
228
|
+
**502** on `/health`, `/info` and `/assistants` (nginx up, app behind it down) after a clean 401
|
|
229
|
+
~10 minutes earlier. Re-probe `/info` before assuming a client bug.
|
|
230
|
+
|
|
205
231
|
## Change history
|
|
232
|
+
- 2026-09-01 — Added a **browser-client** gotcha set found while wiring toga25-supply: Aegra returns
|
|
233
|
+
CORS headers **on the OPTIONS preflight only**, so a browser blocks every real response even on a
|
|
234
|
+
healthy service (server-side callers never see it); `AUTH_TYPE=togahub` **accepts an api2 JWT
|
|
235
|
+
issued for a non-TogaHub client** (`aud` is not enforced), so sibling apps need no token exchange;
|
|
236
|
+
and `assistant_id` is **per-tenant** and must be discovered via `GET /assistants` (pick on
|
|
237
|
+
`metadata.customer_facing`, send no body/`Content-Type`). Also recorded a dated 502 outage
|
|
238
|
+
observation on `api.beta.togaiq.com`. (apeterson)
|
|
206
239
|
- 2026-06-16 — Initial aegra-api feature doc (HTTP layer + execution pipeline). (akhokhani)
|
|
@@ -6,7 +6,7 @@ project: TOGa IQ
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-01
|
|
10
10
|
owners: [apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- talos/package.json
|
|
@@ -154,15 +154,26 @@ rather than an assumption.
|
|
|
154
154
|
## Gotchas
|
|
155
155
|
|
|
156
156
|
- **`@langchain/react` is a different package** from what this app uses. It is a newer wrapper
|
|
157
|
-
(v1.0.33) over the same `@langchain/langgraph-sdk`, and it adds
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
(v1.0.33) over the same `@langchain/langgraph-sdk`, and it adds media hooks/players. Do not assume
|
|
158
|
+
talos validates it — talos uses the SDK's own `/react` subpath.
|
|
159
|
+
- **⚠ Correction (verified 2026-09-01): `@langchain/core` is NOT an extra peer of
|
|
160
|
+
`@langchain/react`.** This doc previously said choosing the SDK's `/react` subpath meant one fewer
|
|
161
|
+
peer. It does not. **`@langchain/langgraph-sdk@1.10.0` declares `@langchain/core: ^1.1.48` as a
|
|
162
|
+
required (non-optional) peer itself** — `react`/`react-dom` are the optional ones. Symptom when
|
|
163
|
+
it is absent: Vite/esbuild dep-optimize hard-fails with `Could not resolve
|
|
164
|
+
"@langchain/core/messages"` and the dev server exits. The **decision** to use the SDK subpath
|
|
165
|
+
still stands (proven in-house, smaller surface) — only the "one fewer peer" reason was wrong.
|
|
160
166
|
- **Do not copy `nuqs`** into a non-Next app (see above).
|
|
161
167
|
- **`env.ts` is the only sanctioned env read** ("never access `process.env` directly elsewhere") and
|
|
162
168
|
`validateEnv()` throws at startup on missing vars. Keep new env reads inside it — but note its
|
|
163
169
|
comments have already drifted from its values once.
|
|
164
170
|
|
|
165
171
|
## Change history
|
|
172
|
+
- 2026-09-01 — Correction only (no talos code change): the recorded rationale that
|
|
173
|
+
`@langchain/langgraph-sdk/react` means **one fewer peer** than `@langchain/react` is **wrong** —
|
|
174
|
+
the SDK declares `@langchain/core ^1.1.48` as a required peer itself, and omitting it hard-fails
|
|
175
|
+
Vite dep-optimize with `Could not resolve "@langchain/core/messages"`. The package choice itself
|
|
176
|
+
is unchanged. Found while building the same stack in toga25-supply. (apeterson)
|
|
166
177
|
- 2026-08-28 — Documented the previously-undocumented TOGa IQ chat front-end: `useStream` from
|
|
167
178
|
`@langchain/langgraph-sdk/react` wrapped in an app-owned context, the browser's **direct
|
|
168
179
|
cross-origin call to Aegra** (proving a static SPA needs no server proxy) and the vestigial
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
| [Primary Table templates (server/client, sizing, virtualization)](features/primary-table-templates.md) | `src/templates/PrimaryTable/` is the **production, wired-up table** built on the [Table component](table.md). | toga-blox/src/components/Table/themeConfig/toga.module.css, toga25-supply/src/themeConfig.json, toga-blox/src/templates/PrimaryTable/PrimaryTable.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableServerTemplate.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableClientTemplate.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableHeaderCell.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableBodyCell.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableRow.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableExpandableRow.tsx, toga-blox/src/templates/PrimaryTable/types.ts |
|
|
12
12
|
| [TableRecordModal (record-detail modal shell)](features/table-record-modal.md) | `TableRecordModal` is a **generic, presentational modal shell** for showing a single table record (row) in detail — typically opened from a table row click. | toga-blox/src/components/TableRecordModal/TableRecordModal.tsx, toga-blox/src/components/TableRecordModal/index.ts, toga-blox/src/components/TableRecordModal/tableRecordModal.module.css |
|
|
13
13
|
| [Table component (cells, action cells, filters & sorts, hooks, theming)](features/table.md) | The `Table` component (`src/components/Table/`) is the **TanStack Table v8** building block behind the [Primary Table templates](primary-table-templates.md). | toga-blox/src/components/Table/hooks/useFetchTablePageMeta.ts, toga-blox/src/api/types.ts, toga-blox/src/components/Table/index.ts, toga-blox/src/components/Table/types.ts, toga-blox/src/components/Table/utils/buildTanstackColumns.tsx, toga-blox/src/components/Table/utils/resolveCellType.tsx, toga-blox/src/components/Table/components/cellTypes, toga-blox/src/components/Table/components/actionCells, toga-blox/src/components/Table/components/columnFiltersAndSorts, toga-blox/src/components/Table/hooks, toga-blox/src/components/Table/themeConfig |
|
|
14
|
-
| [Talos AI-Assistant Component (launcher + docked/drawer chat panel)](features/talos-assistant.md) | `Talos` is a **shared, pure-UI** AI-assistant component in `@agilant/toga-blox`: a header launcher button (`TalosLauncher`) plus a chat panel (`TalosPanel`) tha | toga-blox/src/components/Talos/TalosLauncher.tsx, toga-blox/src/components/Talos/TalosPanel.tsx, toga-blox/src/components/Talos/TalosMessage.tsx, toga-blox/src/components/Talos/types.ts, toga-blox/src/components/Talos/theme.ts, toga-blox/src/components/Talos/helpers.tsx, toga-blox/src/components/Talos/stub.ts, toga-blox/src/components/Talos/Talos.module.css, toga-blox/src/components/Talos/index.ts, toga-blox/src/components/index.ts |
|
|
14
|
+
| [Talos AI-Assistant Component (launcher + docked/drawer chat panel)](features/talos-assistant.md) | `Talos` is a **shared, pure-UI** AI-assistant component in `@agilant/toga-blox`: a header launcher button (`TalosLauncher`) plus a chat panel (`TalosPanel`) tha | toga-blox/src/components/Talos/TalosLauncher.tsx, toga-blox/src/components/Talos/TalosPanel.tsx, toga-blox/src/components/Talos/TalosMessage.tsx, toga-blox/src/components/Talos/types.ts, toga-blox/src/components/Talos/theme.ts, toga-blox/src/components/Talos/helpers.tsx, toga-blox/src/components/Talos/stub.ts, toga-blox/src/components/Talos/Talos.module.css, toga-blox/src/components/Talos/index.ts, toga-blox/src/components/Talos/TalosPanel.test.tsx, toga-blox/src/components/index.ts |
|
|
15
15
|
| [Toaster — host-token styling contract](features/toaster.md) | The blox `Toaster` is a CSS-Module component whose every visual property reads a `--toaster-*` custom property supplied by the **host app**. | toga-blox/src/components/Toaster/Toaster.module.css, toga-blox/src/components/Toaster/Toaster.tsx, toga-blox/src/components/Toaster/ToasterContext.tsx, toga2-commerce/src/styles/index.css, toga2-commerce/src/components/Toasters/ToasterList.tsx |
|
|
16
16
|
| [Dynamic npm Publish Pipeline (branch → channel)](workflows/dynamic-publish-pipeline.md) | How `@agilant/toga-blox` (checkout folder `toga-blox-npm`, registry repo key `toga-blox`) publishes a per-environment npm **channel** (dist-tag) from a `_<mode> | toga-blox/.github/workflows/publish.yml, toga-blox/package.json, toga-blox/src/utils/getFontAwesomeIcon.tsx |
|
|
17
17
|
| [Landing a Large Long-Lived Feature Branch (merge, never rebase)](workflows/landing-a-large-feature-branch.md) | The procedure for landing a long-lived, very large feature branch (months old, hundreds of commits) into `_production` in `toga-blox-npm`. | toga-blox/package.json, toga-blox/.github/workflows/publish.yml |
|
|
@@ -6,7 +6,7 @@ project: TOGa Blox
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-01
|
|
10
10
|
owners: [apeterson, tcox]
|
|
11
11
|
files:
|
|
12
12
|
- toga-blox/src/components/Talos/TalosLauncher.tsx
|
|
@@ -18,6 +18,7 @@ files:
|
|
|
18
18
|
- toga-blox/src/components/Talos/stub.ts
|
|
19
19
|
- toga-blox/src/components/Talos/Talos.module.css
|
|
20
20
|
- toga-blox/src/components/Talos/index.ts
|
|
21
|
+
- toga-blox/src/components/Talos/TalosPanel.test.tsx
|
|
21
22
|
- toga-blox/src/components/index.ts
|
|
22
23
|
related:
|
|
23
24
|
- ../architecture.md
|
|
@@ -40,7 +41,11 @@ across toga25-supply, toga2-desk, toga2-commerce, and `ai-bdr`. Build once, reth
|
|
|
40
41
|
|
|
41
42
|
The defining constraint: the component holds **no business logic and no `window.*` globals** —
|
|
42
43
|
every side effect flows through an injected adapter, and every visual token is overridable.
|
|
43
|
-
|
|
44
|
+
|
|
45
|
+
Since **2026-09-01** the panel also has a **controlled mode**: the host passes `messages` /
|
|
46
|
+
`isStreaming` / `onSubmit` / `onStop` / `connection` and owns the conversation entirely. That is how
|
|
47
|
+
toga25-supply runs the live streaming Aegra backend. **Uncontrolled mode is unchanged** — `onSend`
|
|
48
|
+
still ships stubbed for hosts that pass no `messages`.
|
|
44
49
|
|
|
45
50
|
## Architecture (the four load-bearing decisions)
|
|
46
51
|
|
|
@@ -131,15 +136,27 @@ button, dropping expanded mode) are variants, not theme overrides.
|
|
|
131
136
|
(`"comfortable" | "compact"`), and possibly a launcher `variant` + optional panel-mode props.
|
|
132
137
|
Open questions were handed to the UX/UI team.
|
|
133
138
|
|
|
134
|
-
## Where the code lives
|
|
139
|
+
## Where the code lives — TWO divergent contracts on two branches (verified 2026-09-01)
|
|
140
|
+
|
|
141
|
+
**Talos is on NO mainline branch.** `origin/_main`, `_production`, `_sandbox-client`, `_sandbox-dev`
|
|
142
|
+
and `_beta` all lack `src/components/Talos/types.ts`. It is **in no npm dist-tag** — `latest` is
|
|
143
|
+
`1.0.317`, `production` is `1.1.1-production.139`, neither contains Talos.
|
|
144
|
+
|
|
145
|
+
| Branch | Contract |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `origin/TRUE-80692` | **NARROW** — `paras` required; no `message` / `suggestions` / `cta`; `expanded` / `width` required; no `variant` / `features` |
|
|
148
|
+
| `origin/TRUE-80692-BDR` | **WIDENED** — everything the drawer work added |
|
|
135
149
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
-
|
|
141
|
-
|
|
142
|
-
|
|
150
|
+
> **Correction to the 2026-08-27 record:** tcox's widening commit `484e2a6` is on
|
|
151
|
+
> **`TRUE-80692-BDR`**, *not* on the ticket branch `TRUE-80692`. Anyone branching off
|
|
152
|
+
> `TRUE-80692` expecting the drawer variant gets the narrow contract.
|
|
153
|
+
|
|
154
|
+
The 2026-09-01 controlled-mode work was done on `features-talos-transferorders`, which **merged
|
|
155
|
+
`origin/TRUE-80692-BDR`** first (merge verified not to touch `AdvancedSelect`).
|
|
156
|
+
|
|
157
|
+
- Peers still cap React at `^18`.
|
|
158
|
+
- The blox test suite has **~40 pre-existing failures unrelated to Talos** — the same 40 fail with
|
|
159
|
+
the Talos changes stashed. Do not read them as a regression from this work.
|
|
143
160
|
|
|
144
161
|
## The contract cannot express streaming — a real backend REQUIRES a contract change
|
|
145
162
|
|
|
@@ -161,19 +178,43 @@ Aegra/LangGraph backend (which streams tokens, emits tool calls, and surfaces HI
|
|
|
161
178
|
see [TOGa IQ chat frontend](../../talos/features/chat-frontend.md)) means changing this component's
|
|
162
179
|
props. That was accepted deliberately rather than shipping a non-streaming v1.
|
|
163
180
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
181
|
+
## Controlled mode — BUILT 2026-09-01 (the TRUE-80692 contract change)
|
|
182
|
+
|
|
183
|
+
Implemented on branch `features-talos-transferorders`. New **optional** props on `TalosPanelProps`:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
messages?: TalosMessageModel[];
|
|
187
|
+
isStreaming?: boolean;
|
|
188
|
+
onSubmit?: (text: string) => void;
|
|
189
|
+
onStop?: () => void;
|
|
190
|
+
connection?: TalosConnection; // new exported type
|
|
191
|
+
// export type TalosConnection = "connecting" | "ready" | "error"; default "ready"
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Controlled mode is keyed off `messages` being PASSED AT ALL** — not off a `controlled` flag. So
|
|
195
|
+
every existing consumer and `createTalosStubAdapter()` are untouched. That non-breaking property
|
|
196
|
+
matters: **BDR is a live consumer today.**
|
|
197
|
+
|
|
198
|
+
Behaviour when controlled:
|
|
199
|
+
|
|
200
|
+
- The panel renders the **host's** messages and **never calls `adapter.onSend`**.
|
|
201
|
+
- **It does not append optimistically.** The host's next `messages` already carries the user turn,
|
|
202
|
+
so appending would duplicate it.
|
|
203
|
+
- **`history` and `newChat` are forced OFF** — they act on the panel's own conversation list, which
|
|
204
|
+
it no longer owns.
|
|
205
|
+
- The composer is **disabled until `connection === "ready"`**, with placeholders
|
|
206
|
+
`"Connecting to Talos…"` (connecting) and `"Talos is unavailable right now."` (error).
|
|
207
|
+
- The send button **swaps to a stop button** when `isStreaming && onStop`.
|
|
167
208
|
|
|
168
|
-
|
|
209
|
+
Why this shape was safe to add: `open` / `expanded` / `width` were *already* host-controlled props,
|
|
210
|
+
so moving `messages` to the host is the same pattern, not a new one. And `connection` gives the
|
|
211
|
+
panel a real "we never got a token to begin with" state — previously a thrown `onSend` was its only
|
|
212
|
+
failure path.
|
|
169
213
|
|
|
170
|
-
|
|
171
|
-
`createTalosStubAdapter()` keep working untouched.
|
|
172
|
-
- **Consistent with what is already here.** `open` / `expanded` / `width` are *already* controlled
|
|
173
|
-
props owned by the host; moving `messages` to the host is the same pattern, not a new one.
|
|
214
|
+
### Tests (first ever for this component)
|
|
174
215
|
|
|
175
|
-
|
|
176
|
-
|
|
216
|
+
`TalosPanel.test.tsx` — **8 Vitest + RTL tests**, all passing, including **two that pin
|
|
217
|
+
uncontrolled behaviour as unchanged**. (Separate from the ~40 pre-existing unrelated suite failures.)
|
|
177
218
|
|
|
178
219
|
**The stream itself does NOT belong in blox.** Per
|
|
179
220
|
[frontend.md §21](../../../standards/frontend.md) (promote only after a 2nd consumer validates the
|
|
@@ -185,8 +226,18 @@ Rollup *"failed to resolve import"* for a component they never referenced). blox
|
|
|
185
226
|
|
|
186
227
|
## Gotchas
|
|
187
228
|
|
|
188
|
-
-
|
|
189
|
-
`
|
|
229
|
+
- **⚠ `TalosMessage` reads a DIFFERENT field per role.** A `role: "user"` turn renders
|
|
230
|
+
**`message.text`**; a `role: "talos"` turn renders **`message.message` / `message.paras`**. Put a
|
|
231
|
+
user turn's text in `message` and you get an **empty bubble** — no error, no warning. This bit the
|
|
232
|
+
supply LangGraph mapper and was only caught by writing tests.
|
|
233
|
+
```ts
|
|
234
|
+
{ role: "user", text } // ✅ user turn
|
|
235
|
+
{ role: "talos", message } // ✅ assistant turn
|
|
236
|
+
```
|
|
237
|
+
- **`onSend` cannot stream** — use **controlled mode** (above). A live streaming backend needs the
|
|
238
|
+
controlled props, not a cleverer adapter.
|
|
239
|
+
- **Tool calls and HITL interrupts still have no representation** in `TalosMessageModel`. A host
|
|
240
|
+
mapping from LangGraph must drop them. Surfacing them needs new blox props.
|
|
190
241
|
- The real chat backend is a later blox ticket — `onSend` is stubbed here; do not assume live
|
|
191
242
|
responses from blox alone. **A working implementation exists outside blox and is now wired to
|
|
192
243
|
this component:** `bdr/src/lib/talosChat.ts` implements the same
|
|
@@ -212,6 +263,19 @@ Rollup *"failed to resolve import"* for a component they never referenced). blox
|
|
|
212
263
|
currently consumes it via a local symlink.
|
|
213
264
|
|
|
214
265
|
## Change history
|
|
266
|
+
- 2026-09-01 — **BUILT controlled mode** (the TRUE-80692 contract change, previously only decided):
|
|
267
|
+
optional `messages` / `isStreaming` / `onSubmit` / `onStop` / `connection` on `TalosPanelProps`
|
|
268
|
+
plus a new exported `TalosConnection` type. Controlled mode keys off **`messages` being passed at
|
|
269
|
+
all**, so every existing consumer and `createTalosStubAdapter()` are untouched — non-breaking
|
|
270
|
+
matters because **BDR is live on this component**. Controlled panels never call `adapter.onSend`,
|
|
271
|
+
never append optimistically (the host's next `messages` already has the user turn), force
|
|
272
|
+
`history`/`newChat` off, disable the composer until `connection === "ready"`, and swap send→stop
|
|
273
|
+
while streaming. Added the component's **first tests** (`TalosPanel.test.tsx`, 8 Vitest+RTL, two
|
|
274
|
+
pinning uncontrolled behaviour). FIXED a real rendering trap found by those tests: `TalosMessage`
|
|
275
|
+
reads **`text` for user turns** but **`message`/`paras` for Talos turns**, so a user turn written
|
|
276
|
+
to `message` renders an empty bubble. DISCOVERED the branch topology is not what was recorded —
|
|
277
|
+
Talos is on **no** mainline branch, and tcox's widening commit lives on **`TRUE-80692-BDR`**, not
|
|
278
|
+
on `TRUE-80692`; the two branches carry **divergent contracts**. (apeterson)
|
|
215
279
|
- 2026-08-28 — Review only (no blox code change): established that the `onSend(text) =>
|
|
216
280
|
Promise<TalosResponse>` contract **cannot express token streaming** — the 08-27 `message`/
|
|
217
281
|
`suggestions`/`cta` additions widen the *payload* but it still resolves once — so a live
|
|
@@ -6,7 +6,7 @@ project: TOGa Blox
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-01
|
|
10
10
|
owners: [tcox, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- toga-blox/package.json
|
|
@@ -45,6 +45,35 @@ npm creates a **junction** at `<app>/node_modules/@agilant/toga-blox` pointing a
|
|
|
45
45
|
`mklink /J` by hand works too. Use a **junction, not `mklink /D`** — a directory symlink needs
|
|
46
46
|
administrator rights; a junction does not.
|
|
47
47
|
|
|
48
|
+
### 1a. ⚠ EVERY `npm install` in the consumer destroys the link — re-link after each one
|
|
49
|
+
|
|
50
|
+
`npm install` (even installing an unrelated package) replaces the
|
|
51
|
+
`node_modules/@agilant/toga-blox` **symlink** with the registry version. On toga25-supply that is
|
|
52
|
+
`1.1.1-production.139`, which contains **no Talos at all**.
|
|
53
|
+
|
|
54
|
+
The symptom looks nothing like a dependency problem:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
Uncaught SyntaxError: The requested module
|
|
58
|
+
'/node_modules/.vite/deps/@agilant_toga-blox.js' does not provide an export named 'TalosLauncher'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Re-link (from the consumer root):**
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
rm -rf node_modules/@agilant/toga-blox
|
|
65
|
+
ln -s ../../../toga-blox-npm node_modules/@agilant/toga-blox
|
|
66
|
+
rm -rf node_modules/.vite
|
|
67
|
+
# then restart the dev server
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- **`ln -sfn` alone is NOT enough** when the path is a real directory — it creates the link
|
|
71
|
+
*inside* it. You must `rm -rf` first.
|
|
72
|
+
- Clearing `node_modules/.vite` is required because the stale pre-bundle is served `immutable`
|
|
73
|
+
(see §2c).
|
|
74
|
+
- Same rule after editing blox source: `npm run build` in `toga-blox-npm`, **then** clear
|
|
75
|
+
`node_modules/.vite` in the consumer.
|
|
76
|
+
|
|
48
77
|
### 2. Collapse to ONE React (the React 19 case)
|
|
49
78
|
|
|
50
79
|
`toga-blox-npm/node_modules` ships its own `react` + `react-dom` **18.3.1**. Through a link,
|
|
@@ -207,6 +236,11 @@ The consumer reads blox's `dist`, so rebuild blox after every change.
|
|
|
207
236
|
drags every component's CSS into the consumer bundle.
|
|
208
237
|
|
|
209
238
|
## Change history
|
|
239
|
+
- 2026-09-01 — Recorded that **any `npm install` in the consumer silently replaces the blox symlink
|
|
240
|
+
with the registry version** (toga25-supply → `1.1.1-production.139`, which has no Talos), and that
|
|
241
|
+
the symptom is a misleading `does not provide an export named 'TalosLauncher'`. Added the re-link
|
|
242
|
+
recipe and the trap that **`ln -sfn` alone fails when the target path is a real directory** — it
|
|
243
|
+
creates the link inside it, so `rm -rf` first. (apeterson)
|
|
210
244
|
- 2026-08-31 — Added the "is it actually linked?" check: `toga25-supply` is on a **real registry
|
|
211
245
|
install** of `1.1.1-production.139` (`LinkType` empty), not a junction, so blox source edits reach
|
|
212
246
|
it only through publish + pin bump; and recorded that the urgent-case **installed-`dist` patch** is
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
|
-
| [TOGa 2.5 Supply — Architecture](architecture.md) | `toga25-supply` ("TOGa 2.5 Supply") is the **React/TypeScript frontend** for the 2.0 Supply application — an iteration and improvement of `toga2-supply`. | toga25-supply/src/main.tsx, toga25-supply/src/App.tsx, toga25-supply/src/routes.tsx, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/layout/, toga25-supply/src/pages/, toga25-supply/src/hooks/useTableCellInteractions.ts |
|
|
5
|
+
| [TOGa 2.5 Supply — Architecture](architecture.md) | `toga25-supply` ("TOGa 2.5 Supply") is the **React/TypeScript frontend** for the 2.0 Supply application — an iteration and improvement of `toga2-supply`. | toga25-supply/src/main.tsx, toga25-supply/src/App.tsx, toga25-supply/src/routes.tsx, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/layout/, toga25-supply/src/pages/, toga25-supply/src/hooks/useTableCellInteractions.ts, toga25-supply/src/providers/TalosStreamProvider.tsx |
|
|
6
6
|
| [Action-Button Rule Engine (Flag / Rule grammar)](features/action-button-rule-engine.md) | A declarative, fully config-driven rule engine that resolves the boolean-ish flags (`isEnabled`, `isVisible`, `isComplete`) on SalesOrder action-button options. | toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.test.ts, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/layout/RecordApprovalModal/view/ApprovalFlowDetailInputs.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/FIELDS/COMPASS/approvalActionFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/FIELDS/COMPASSCANADA/approvalActionFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
|
|
7
7
|
| [Client API-Fetch Fields (useClientApiFields / apiFields.json)](features/client-api-fetch-fields.md) | The mechanism that resolves a client's **API-fetch projection** — which `fields` / `ojoin` / `join` / `where` to request from the TOGa API for a given page — fr | toga25-supply/src/fieldsConfig/resolveApiConfig.ts, toga25-supply/src/fieldsConfig/useClientApiFields.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx |
|
|
8
8
|
| [Client-Configurable Fields (useClientFields / fieldsConfig)](features/client-configurable-fields.md) | The mechanism for config that **varies by client** (or client × role) — field overrides, filter buttons, group-by options, column pickers, layout toggles — with | toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/README.md, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/layout/VendorItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/ |
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
| [Side navigation & default route — an empty nav renders a BLANK PAGE and gets reported as "cannot log in"](features/side-navigation-and-default-route.md) | The 2.5 side nav is **100% backend-driven** by the `navigation` surface bundle, and the same list also decides **which routes exist** and **where `/` lands**. | toga25-supply/src/routes.tsx, toga25-supply/src/layout/AppLayout/viewModel/useAppLayoutViewModel.ts, api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
|
|
14
14
|
| [SSO redirect & public-vs-user session gating (useAuthenticationFlow)](features/sso-redirect-and-session-gating.md) | How 2.5 Supply decides, on every navigation, whether an anonymous visitor should be bounced to their client's SSO IdP instead of the local `/login` form. | toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/src/routes.tsx, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/api/api.ts |
|
|
15
15
|
| [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/App.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/SalesOrderRecordModal.module.css, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/SalesOrderApprovalModalsLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/RecordApprovalModalLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx, toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css, toga25-supply/src/utils/formatDateTime.ts, toga25-supply/src/utils/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/layout/RecordApprovalModal/, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/surface/applyColSpan.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/sectionRenderers.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/ServiceRequestRecordModalLayout.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/useServiceRequestRecordModalLayoutModel.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx, toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/AdminNotesSection.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getAdminNotes.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/resolveElementState.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SurfaceRowActions.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderVip.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx, toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/src/surface/SurfaceHeader.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
|
|
16
|
-
| [Talos Integration (AppLayout host
|
|
16
|
+
| [Talos Integration (AppLayout host, live Aegra streaming, LangGraph→blox mapper)](features/talos-integration.md) | toga25-supply is the first host of the shared blox [Talos assistant](../../toga-blox/features/talos-assistant.md). | toga25-supply/src/api/talos.ts, toga25-supply/src/hooks/useTalosSession.ts, toga25-supply/src/providers/TalosStreamProvider.tsx, toga25-supply/src/utils/talosMessages.ts, toga25-supply/src/utils/talosMessages.test.ts, toga25-supply/src/layout/AppLayout/AppLayout.tsx, toga25-supply/src/components/Header/Header.tsx, toga25-supply/src/components/Header/Header.module.css, toga25-supply/src/index.css, toga25-supply/src/assets/talos-owl.png |
|
|
17
17
|
| [Transfer Orders page (TableView → Core surfaces → React page + record modal)](features/transfer-orders-page.md) | The Transfer Orders screen — list + read-only record modal — built end to end on 2026-08-28 from a Claude Design prototype. | toga25-supply/src/pages/TransferOrders/TransferOrders.tsx, toga25-supply/src/pages/TransferOrders/hooks/useTransferOrdersTableState.ts, toga25-supply/src/pages/TransferOrders/viewModel/useTransferOrdersPageViewModel.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrdersTableLayout/TransferOrdersTableLayout.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/TransferOrderRecordModalLayout.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/hooks/useTransferOrderRecord.ts, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/viewModel/useTransferOrderRecordModalLayoutModel.ts, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/view/TransferOrderRecordView.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/CreateTransferOrderModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/index.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/usePurchaseOrderItemRows.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTargetLocationOptions.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTransferSourceLocation.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useLocationContacts.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateLocationContact.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/AddContactModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/TransferNotesField.tsx, dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql, toga25-supply/src/surface/useColumnVisibilityModalConfig.ts, toga25-supply/src/routes.tsx, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql |
|
|
18
18
|
| [AWS Amplify Multi-Environment Deployment](workflows/amplify-deployment.md) | How `toga25-supply` deploys to **all** of its environments on AWS Amplify from a **single shared `amplify.yml`**. | toga25-supply/amplify.yml, toga25-supply/src/api/api.ts, toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/vite.config.ts, toga25-supply/package.json, toga25-supply/.env.development |
|
|
19
19
|
| [Cypress Testing Harness (component + e2e)](workflows/cypress-testing.md) | The Cypress test harness for the `toga25-supply` frontend, bootstrapped from scratch (`cypress` was already a dependency but there was no config, no `cypress/` | toga25-supply/cypress.config.ts, toga25-supply/cypress/tsconfig.json, toga25-supply/cypress/support/component.tsx, toga25-supply/cypress/support/component-index.html, toga25-supply/cypress/support/e2e.ts, toga25-supply/cypress/support/commands.ts, toga25-supply/cypress/support/fixtures.ts, toga25-supply/cypress/support/mocks/useApprovalModalViewModel.ts, toga25-supply/cypress/component/SalesOrderApprovalModalsLayout.cy.tsx, toga25-supply/cypress/component/RecordApprovalModalLayout.cy.tsx, toga25-supply/cypress/component/EnterPoNumberModal.cy.tsx, toga25-supply/cypress/e2e/salesOrderApproval.cy.ts |
|
|
@@ -17,7 +17,9 @@ files:
|
|
|
17
17
|
- toga25-supply/src/layout/
|
|
18
18
|
- toga25-supply/src/pages/
|
|
19
19
|
- toga25-supply/src/hooks/useTableCellInteractions.ts
|
|
20
|
+
- toga25-supply/src/providers/TalosStreamProvider.tsx
|
|
20
21
|
related:
|
|
22
|
+
- features/talos-integration.md
|
|
21
23
|
- features/client-configurable-fields.md
|
|
22
24
|
- features/meta-driven-table-data.md
|
|
23
25
|
- features/column-visibility.md
|
|
@@ -118,5 +120,14 @@ its primary consumer and resolves it via a symlinked `node_modules/@agilant/toga
|
|
|
118
120
|
can serve a persisted entry from a **previous session** and never fetch — the symptom is an empty
|
|
119
121
|
list/dropdown with **nothing in the network tab**, which reads as a backend fault and is not one.
|
|
120
122
|
(`staleTime: Infinity` on the **surface-meta** hooks is a deliberate, separate exception.)
|
|
121
|
-
-
|
|
122
|
-
`
|
|
123
|
+
- **⚠ `npx tsc --noEmit` at the repo root checks NOTHING — it is a silent no-op.** Verified
|
|
124
|
+
2026-09-01: the root `tsconfig.json` is references-only (`files: []`), so it compiles nothing
|
|
125
|
+
and **always exits 0**, reporting success on a codebase full of type errors.
|
|
126
|
+
**Always type-check with `npx tsc --noEmit -p tsconfig.app.json`** (or `npx tsc -b`, which
|
|
127
|
+
walks the references). Adding a required key to `ClientFields` then surfaces every bundle
|
|
128
|
+
missing it. The identical trap is documented for `toga2-view`
|
|
129
|
+
([architecture](../toga2-view/architecture.md)); **any app sharing this tsconfig shape (likely
|
|
130
|
+
`toga25-desk`) is probably affected too — verify before asserting it.**
|
|
131
|
+
- **Talos streaming lives in `src/providers/TalosStreamProvider.tsx`, above the router outlet.**
|
|
132
|
+
`AppLayout` wraps `AppLayoutInner` so it can consume the provider it renders. See
|
|
133
|
+
[talos-integration](features/talos-integration.md).
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Talos Integration (AppLayout host
|
|
2
|
+
title: Talos Integration (AppLayout host, live Aegra streaming, LangGraph→blox mapper)
|
|
3
3
|
framework: "2.0"
|
|
4
4
|
repo: toga25-supply
|
|
5
5
|
project: TOGa 2.5 Supply
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-01
|
|
10
10
|
owners: [apeterson]
|
|
11
11
|
files:
|
|
12
|
+
- toga25-supply/src/api/talos.ts
|
|
13
|
+
- toga25-supply/src/hooks/useTalosSession.ts
|
|
14
|
+
- toga25-supply/src/providers/TalosStreamProvider.tsx
|
|
15
|
+
- toga25-supply/src/utils/talosMessages.ts
|
|
16
|
+
- toga25-supply/src/utils/talosMessages.test.ts
|
|
12
17
|
- toga25-supply/src/layout/AppLayout/AppLayout.tsx
|
|
13
18
|
- toga25-supply/src/components/Header/Header.tsx
|
|
14
19
|
- toga25-supply/src/components/Header/Header.module.css
|
|
@@ -16,132 +21,206 @@ files:
|
|
|
16
21
|
- toga25-supply/src/assets/talos-owl.png
|
|
17
22
|
related:
|
|
18
23
|
- ../../toga-blox/features/talos-assistant.md
|
|
24
|
+
- ../../toga-blox/workflows/local-link-into-a-consumer-app.md
|
|
19
25
|
- ../../talos/features/chat-frontend.md
|
|
26
|
+
- ../../talos/features/aegra-api.md
|
|
20
27
|
- ../../../standards/frontend.md
|
|
21
28
|
---
|
|
22
29
|
|
|
23
30
|
## Summary
|
|
24
31
|
|
|
25
32
|
toga25-supply is the first host of the shared blox [Talos assistant](../../toga-blox/features/talos-assistant.md).
|
|
26
|
-
**Enabled for ALL clients (no per-client gating).** `AppLayout` is the host:
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
**Enabled for ALL clients (no per-client gating).** `AppLayout` is the host: the launcher renders
|
|
34
|
+
in the header, the panel at app root.
|
|
35
|
+
|
|
36
|
+
As of **2026-09-01 the live Aegra/LangGraph streaming backend is BUILT** (TRUE-80693). `onSend` is
|
|
37
|
+
no longer a stub — the panel runs in blox's new **controlled mode** and the stream lives in a
|
|
38
|
+
supply-owned provider. All LangChain code is **app-local**; blox stays pure UI.
|
|
29
39
|
|
|
30
40
|
## How it works
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
- **Rendering** — `<TalosLauncher>` renders in the header via a new presentational
|
|
40
|
-
`talosLauncher` slot prop on the blox `Header` component, laid out through a `.headerActions`
|
|
41
|
-
flex wrapper. `<TalosPanel>` renders at app root.
|
|
42
|
-
- **Branding/font** — the owl asset (`src/assets/talos-owl.png`) is passed as
|
|
43
|
-
`branding.avatarSrc`; Plus Jakarta Sans is loaded in `src/index.css` (the shared component
|
|
44
|
-
does not import fonts itself).
|
|
42
|
+
```
|
|
43
|
+
src/api/talos.ts — sole VITE_TALOS_API read chokepoint + assistants lookup
|
|
44
|
+
src/hooks/useTalosSession.ts — resolves token + assistantId → a `connection` state
|
|
45
|
+
src/providers/TalosStreamProvider.tsx — the single useStream() call, exposed via own context
|
|
46
|
+
src/utils/talosMessages.ts — LangGraph Message[] → blox TalosMessageModel[]
|
|
47
|
+
src/layout/AppLayout/AppLayout.tsx — host: provider wrapper + controlled panel props
|
|
48
|
+
```
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
### `src/api/talos.ts` — the env chokepoint
|
|
47
51
|
|
|
48
|
-
The
|
|
49
|
-
|
|
50
|
-
header's right edge sitting **under** the panel, hiding the launcher + avatar. **Fix:** apply
|
|
51
|
-
the reserved dock width (`marginRight` = the `onDockWidthChange` value) to the **outer app
|
|
52
|
-
wrapper** so the header shifts along with the content.
|
|
52
|
+
The only place `VITE_TALOS_API` is read. Exposes `getTalosApiUrl()`, `isTalosConfigured()`,
|
|
53
|
+
`fetchTalosAssistants()`, `pickTalosAssistant()`.
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
`fetchTalosAssistants()` **logs the HTTP status before throwing** — `401` = token/issuer mismatch,
|
|
56
|
+
`403` = tenant not provisioned. Without that log React Query reduces both to a silent boolean and
|
|
57
|
+
you debug blind.
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
**Do NOT send a body or a `Content-Type` on the `GET /assistants` call.** `fetch` forbids a body on
|
|
60
|
+
GET, and a JSON content-type only adds a CORS preflight. (The curl example circulated internally
|
|
61
|
+
used `--data '{}'`; do not copy it into the browser client.)
|
|
59
62
|
|
|
60
|
-
###
|
|
63
|
+
### `assistantId` is PER-TENANT — fetch it, never pin it in env
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
its keep, so revisit it then, not now.
|
|
70
|
-
- **Write our own context provider** (as talos does) rather than the library's `StreamProvider` —
|
|
71
|
-
supply-specific token / tenant / `threadId` / Aegra URL must be injected.
|
|
72
|
-
- **It stays in supply, never in blox.** Per [frontend.md §21](../../../standards/frontend.md) this
|
|
73
|
-
is n=1, and per §13(b) a blox peer would force `@langchain/core` on all five consumer apps. blox
|
|
74
|
-
keeps `TalosPanel` as pure UI.
|
|
65
|
+
`GET {aegra}/assistants?limit=100&offset=0` with the bearer returns:
|
|
66
|
+
|
|
67
|
+
```jsonc
|
|
68
|
+
{ "assistants": [ { "assistant_id": "…", "name": "…", "graph_id": "…",
|
|
69
|
+
"metadata": { "customer_facing": true, "read_only": true, "tenant_slug": "…" } } ],
|
|
70
|
+
"total": 1 }
|
|
71
|
+
```
|
|
75
72
|
|
|
76
|
-
|
|
73
|
+
NYCHH returns exactly one — name `TOGa Supply`, `graph_id: deep_agent`, `read_only: true`,
|
|
74
|
+
`customer_facing: true`.
|
|
77
75
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
it must sit **above the router outlet**.
|
|
82
|
-
3. **Be the app↔blox boundary.** Supply's token/tenant/`threadId`/Aegra URL enter here and LangGraph
|
|
83
|
-
message shapes are translated here, keeping `TalosPanel` app-agnostic and reusable by desk and
|
|
84
|
-
commerce later.
|
|
76
|
+
**Selection rule (`pickTalosAssistant`):** prefer `metadata.customer_facing === true`; else fall
|
|
77
|
+
back to the full list; take the first; `console.warn` when there is more than one candidate.
|
|
78
|
+
Never silently pick, never throw.
|
|
85
79
|
|
|
86
|
-
|
|
80
|
+
**Design consequence:** `useStream` takes **both** the token and `assistantId` as *values*, so there
|
|
81
|
+
are now **two** async prerequisites that must resolve above the hook. That is why the panel's
|
|
82
|
+
`connection` prop is load-bearing rather than polish.
|
|
87
83
|
|
|
84
|
+
### `useTalosSession()` — the gate
|
|
85
|
+
|
|
86
|
+
Returns `connection: "disabled" | "connecting" | "ready" | "error"` plus the token and assistantId.
|
|
87
|
+
|
|
88
|
+
- **Gate on `isAuthenticated` (the `"user"` key), NOT on the presence of an accessToken.** blox
|
|
89
|
+
`fetchPublicToken` leaves a **public** token in storage for logged-out visitors, so "there is a
|
|
90
|
+
token" is not "there is a user."
|
|
91
|
+
- **Key the assistants query by USER UUID, never by the token.** Supply persists the React Query
|
|
92
|
+
cache to `localStorage` (`supply-chain-query-cache`), so a token in a query key gets written to
|
|
93
|
+
disk.
|
|
94
|
+
- `staleTime: Infinity`, `gcTime: Infinity`, `retry: false`.
|
|
95
|
+
- **Anti-pattern — do not gate with `useSuspenseQuery`/Suspense.** It suspends the subtree, so the
|
|
96
|
+
panel **unmounts and remounts** — flashing and dropping whatever the user typed in the composer.
|
|
97
|
+
Keep it a plain query and gate on the returned `connection`.
|
|
98
|
+
|
|
99
|
+
### `TalosStreamProvider` — outer gate + inner session
|
|
100
|
+
|
|
101
|
+
One `useStream` call from `@langchain/langgraph-sdk/react`, exposed through a supply-owned context.
|
|
102
|
+
`threadId` lives in the URL via react-router `useSearchParams` (**never `nuqs`** — Next-only).
|
|
103
|
+
|
|
104
|
+
It is deliberately **split in two**: an outer gate component and an inner `TalosStreamSession`.
|
|
105
|
+
Hooks cannot be called conditionally, and the stream must not mount until the token *and* the
|
|
106
|
+
assistantId are real values — so the gate renders the session only once both resolve.
|
|
107
|
+
|
|
108
|
+
Why a provider at all: `useStream` is stateful (two call sites = two conversations), it must sit
|
|
109
|
+
**above the router outlet** so navigation does not kill an in-flight response, and it is the
|
|
110
|
+
app↔blox boundary where LangGraph shapes are translated.
|
|
111
|
+
|
|
112
|
+
### `AppLayout` — split into `AppLayoutInner`
|
|
113
|
+
|
|
114
|
+
`AppLayout` now wraps `AppLayoutInner` in `TalosStreamProvider`, **because a component cannot
|
|
115
|
+
consume a provider it renders**. The panel is driven by the controlled props
|
|
116
|
+
(`messages` / `isStreaming` / `onSubmit` / `onStop` / `connection`); the adapter now carries only
|
|
117
|
+
navigation side effects (`onOpenRecord` → `/sales-orders?sales-orders=<id>`, `onAction("approvals")`
|
|
118
|
+
→ `/sales-orders`, `onDockWidthChange` → reserve layout width).
|
|
119
|
+
|
|
120
|
+
**Maps the app-only `"disabled"` state to `"error"`** when passing `connection` down — blox has no
|
|
121
|
+
`"disabled"` value.
|
|
122
|
+
|
|
123
|
+
### `talosMessages.ts` — the LangGraph → blox mapper
|
|
124
|
+
|
|
125
|
+
- Handles LangGraph's **string-OR-content-blocks** union for `content`.
|
|
126
|
+
- Filters to `human` / `ai` only — `system`, `tool` and `remove` are dropped.
|
|
127
|
+
- An **empty AI turn becomes `{ thinking: true }`** (the stream opening). An empty human turn is
|
|
128
|
+
dropped.
|
|
129
|
+
- Streamed text goes to **`message`** (light markdown), *not* `paras`: during a stream the text is
|
|
130
|
+
partial, and splitting it into finished paragraphs reflows the bubble on every token.
|
|
131
|
+
- **Deliberate loss:** tool calls and HITL interrupts have **no representation** in
|
|
132
|
+
`TalosMessageModel` and are dropped. Surfacing them needs new blox props — not a mapper change.
|
|
133
|
+
|
|
134
|
+
13 unit tests in `src/utils/talosMessages.test.ts`.
|
|
135
|
+
|
|
136
|
+
## Gotcha — user turns use `text`, Talos turns use `message`/`paras`
|
|
137
|
+
|
|
138
|
+
blox `TalosMessage` renders **`message.text` for a `role: "user"` turn** but
|
|
139
|
+
**`message.message` / `message.paras` for a `role: "talos"` turn**. The mapper originally put user
|
|
140
|
+
turns in `message`, which renders an **empty bubble**. Caught only by writing the tests.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
// CORRECT
|
|
144
|
+
{ role: "user", text } // user turn
|
|
145
|
+
{ role: "talos", message } // assistant turn
|
|
88
146
|
```
|
|
89
|
-
src/api/talos.ts — sole VITE_TALOS_API read chokepoint + token exchange
|
|
90
|
-
src/providers/TalosStreamProvider.tsx — the single useStream call, exposed via context
|
|
91
|
-
src/layout/AppLayout/viewModel/useTalosViewModel.ts — flat shape for the panel
|
|
92
|
-
```
|
|
93
147
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
**
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
for
|
|
141
|
-
|
|
142
|
-
|
|
148
|
+
Anyone writing to `TalosMessageModel` by hand will hit this.
|
|
149
|
+
|
|
150
|
+
## Gotcha — reserve dock width on the OUTER wrapper, not just `<main>`
|
|
151
|
+
|
|
152
|
+
The docked panel is `position: fixed`, full-height on the right. The blox `Header` is in **normal
|
|
153
|
+
document flow** (NOT fixed). Reserving the dock width only on `<main>` leaves the header's right
|
|
154
|
+
edge sitting **under** the panel, hiding the launcher + avatar. **Fix:** apply the reserved dock
|
|
155
|
+
width (`marginRight` = the `onDockWidthChange` value) to the **outer app wrapper**.
|
|
156
|
+
|
|
157
|
+
## Gotcha — `npm install` silently un-links blox and Talos disappears
|
|
158
|
+
|
|
159
|
+
Any `npm install` in this repo replaces the `node_modules/@agilant/toga-blox` **symlink** with the
|
|
160
|
+
registry version, which contains no Talos. The symptom looks nothing like a dependency problem.
|
|
161
|
+
Full symptom + relink recipe:
|
|
162
|
+
[Local-linking blox into a consumer app §1a](../../toga-blox/workflows/local-link-into-a-consumer-app.md).
|
|
163
|
+
|
|
164
|
+
## Environment config
|
|
165
|
+
|
|
166
|
+
`VITE_TALOS_API` is set to `https://api.beta.togaiq.com` in **7 non-prod env files**. It is left
|
|
167
|
+
**commented out in `.env` (production)** because no live production Aegra host is confirmed —
|
|
168
|
+
`isTalosConfigured()` therefore returns false there and `connection` resolves to `"disabled"`.
|
|
169
|
+
|
|
170
|
+
## Package choice: `@langchain/langgraph-sdk/react`, app-local
|
|
171
|
+
|
|
172
|
+
- **`useStream` from `@langchain/langgraph-sdk/react`** — the same import talos uses — **not
|
|
173
|
+
`@langchain/react`.** The decision stands: the path is proven in-house so
|
|
174
|
+
`talos/src/providers/Stream.tsx` lifts nearly verbatim, and `@langchain/react`'s extra surface is
|
|
175
|
+
largely **media hooks/players** blox has no components to render.
|
|
176
|
+
- **⚠ Correction (verified 2026-09-01): the old "one fewer peer" reason was WRONG.**
|
|
177
|
+
`@langchain/langgraph-sdk@1.10.0` declares `@langchain/core: ^1.1.48` as a **required
|
|
178
|
+
(non-optional) peer** itself; `react`/`react-dom` are the optional ones. Symptom when it is
|
|
179
|
+
missing: Vite/esbuild dep-optimize **hard-fails** with `Could not resolve
|
|
180
|
+
"@langchain/core/messages"` and the dev server exits. Install `@langchain/core` alongside the SDK.
|
|
181
|
+
- **Take only `useStream` for v1.** Add `useToolCalls` when tool activity is actually shown, and the
|
|
182
|
+
headless interrupt helpers for human-in-the-loop — that is when `@langchain/react` earns its keep.
|
|
183
|
+
- **It stays in supply, never in blox.** Per [frontend.md §21](../../../standards/frontend.md) this
|
|
184
|
+
is n=1, and per §13(b) a blox peer would force `@langchain/core` on all five consumer apps.
|
|
185
|
+
|
|
186
|
+
**Still open:** mid-stream 401 behavior — silent refresh and resume vs. surface an error and let the
|
|
187
|
+
user resend. Decide deliberately rather than discovering it in QA.
|
|
188
|
+
|
|
189
|
+
## Aegra-side status (was "three blockers")
|
|
190
|
+
|
|
191
|
+
- **(a) CORS — NARROWED, still open.** The old worry that Aegra's `Domains` gate needs a spoofed
|
|
192
|
+
`Origin` (talos pins `Origin: http://talos` server-side, and `Origin` is a forbidden header) is
|
|
193
|
+
**moot for supply**: supply's real browser origin *is* the registered hostname, so the browser
|
|
194
|
+
sets it correctly for free. What **is** still a blocker: **Aegra returns CORS headers on the
|
|
195
|
+
OPTIONS preflight only, not on the actual response** — verified 2026-09-01 from both
|
|
196
|
+
`http://nychh.dev.sandbox.togasupply:5173` and `https://nychh.togasupply.com`
|
|
197
|
+
(`access-control-allow-origin: *` on the preflight, **no** `access-control-*` header on the real
|
|
198
|
+
GET). The browser will block every real response until this is fixed. Reported to `akhokhani`.
|
|
199
|
+
- **(b) Tenant provisioning is explicit, no auto-create.** Unchanged — an unregistered org gets a
|
|
200
|
+
clean **403 on every call even with a valid token**.
|
|
201
|
+
- **(c) Token audience — RESOLVED, and it is the good outcome.** Aegra `AUTH_TYPE=togahub`
|
|
202
|
+
**accepts a supply-issued api2 JWT.** Proven by a working `GET /assistants` whose bearer carried
|
|
203
|
+
`aud` = a **supply** hostname (`https://nychh.togasupply.com`), `iss`/`sub` =
|
|
204
|
+
`http://api.beta.togahub.com`, `id.client.slug: Nychh`, 3600s life. Answer from `akhokhani`.
|
|
205
|
+
**Consequence: no token exchange, no `/auth/delegator` handoff, no auth-exchange layer.** Supply
|
|
206
|
+
passes the token it already has straight into `defaultHeaders`.
|
|
143
207
|
|
|
144
208
|
## Change history
|
|
209
|
+
- 2026-09-01 — **BUILT the live streaming integration.** Added `src/api/talos.ts` (env chokepoint +
|
|
210
|
+
runtime `GET /assistants` lookup — `assistantId` is **per-tenant**, never pinned in env),
|
|
211
|
+
`useTalosSession` (gates on `isAuthenticated`, not on a token, because blox leaves a **public**
|
|
212
|
+
token in storage; assistants query keyed by user UUID because the query cache is persisted to
|
|
213
|
+
disk), `TalosStreamProvider` (one `useStream`, own context, `threadId` via react-router
|
|
214
|
+
`useSearchParams`, split into outer gate + inner session because hooks can't be conditional), the
|
|
215
|
+
LangGraph→blox mapper, and `AppLayout` → `AppLayoutInner` (a component can't consume a provider it
|
|
216
|
+
renders). RESOLVED blocker (c): Aegra `AUTH_TYPE=togahub` accepts a **supply-issued** api2 JWT
|
|
217
|
+
(`aud` = a supply hostname), so there is no token exchange. Narrowed blocker (a): the `Origin`
|
|
218
|
+
spoof worry is moot for supply, but Aegra sends CORS headers **on the preflight only**, so real
|
|
219
|
+
responses are still browser-blocked. FIXED a real bug found by the new tests: blox renders
|
|
220
|
+
`text` for user turns and `message`/`paras` for Talos turns — the mapper's original shape rendered
|
|
221
|
+
an empty user bubble. CORRECTED the recorded package rationale: `@langchain/langgraph-sdk`
|
|
222
|
+
**does** require `@langchain/core ^1.1.48` as a peer, so "one fewer peer" was wrong (the decision
|
|
223
|
+
stands for other reasons). (apeterson)
|
|
145
224
|
- 2026-08-28 — Design/research only, no code: chose `useStream` from
|
|
146
225
|
`@langchain/langgraph-sdk/react` (not `@langchain/react`), kept **app-local** rather than in blox
|
|
147
226
|
(frontend.md §13b/§21), and designed a route-surviving singleton `TalosStreamProvider` above the
|
|
@@ -5,7 +5,7 @@ project: _Underscore
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-
|
|
8
|
+
updated: 2026-09-01
|
|
9
9
|
owners: [jcardinal, apeterson]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
@@ -122,6 +122,22 @@ ship the **identical** app-config flag set — the two files differ only in whit
|
|
|
122
122
|
`resolve.alias`). A path alias that exists in only one of the two type-checks-but-won't-build (or
|
|
123
123
|
the reverse). Keep them in lockstep.
|
|
124
124
|
|
|
125
|
+
**MUST: type-check with `-p tsconfig.app.json` (or `tsc -b`) — never bare `npx tsc --noEmit`.**
|
|
126
|
+
Because the root `tsconfig.json` is references-only (`files: []`), a bare root `tsc --noEmit`
|
|
127
|
+
compiles nothing and **always exits 0**. It reports a clean build on a repo full of type errors,
|
|
128
|
+
which makes it a false green in any pre-commit or CI-style check.
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npx tsc --noEmit -p tsconfig.app.json # ✅
|
|
132
|
+
npx tsc --noEmit # ❌ silent no-op
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Documented independently in two repos on this tsconfig shape — `toga25-supply` (verified
|
|
136
|
+
2026-09-01) and `toga2-view` ([architecture](../apps/toga2-view/architecture.md)) — so assume it
|
|
137
|
+
for any app sharing the shape, but **verify per repo before asserting it**. Note also that a repo
|
|
138
|
+
may carry **pre-existing** errors under the app config (toga2-view does), in which case the pass
|
|
139
|
+
criterion is "no *new* errors vs. a stashed baseline", not zero.
|
|
140
|
+
|
|
125
141
|
## 3. State management doctrine — **MUST** (high-value)
|
|
126
142
|
|
|
127
143
|
There are exactly **three homes** for state, and they do not overlap. Verified 2026-08-18 across
|
|
@@ -664,6 +680,11 @@ consuming app, guard against them and move on.
|
|
|
664
680
|
|
|
665
681
|
## Change history
|
|
666
682
|
|
|
683
|
+
- 2026-09-01 — §2: added a MUST to **type-check with `-p tsconfig.app.json` (or `tsc -b`), never
|
|
684
|
+
bare `npx tsc --noEmit`** — the root `tsconfig.json` is references-only (`files: []`), so a bare
|
|
685
|
+
root run compiles nothing and always exits 0, a false green in any pre-commit/CI check. Verified
|
|
686
|
+
in `toga25-supply` 2026-09-01 and already documented for `toga2-view`; assume it for any app on
|
|
687
|
+
this tsconfig shape but verify per repo. (apeterson)
|
|
667
688
|
- 2026-08-27 — §13(a): corrected the caret rationale — a caret on a **prerelease is inert**, not
|
|
668
689
|
floating; the real floating risk is `toga2-commerce`'s `amplify.yml` overwriting the manifest pin
|
|
669
690
|
at build time. §22: added three blox auth gotchas found debugging toga25-supply SSO
|
|
@@ -196,4 +196,12 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
|
|
|
196
196
|
numbers — because it does **not** use NetSuite, so it never hit the inventory-adjustment
|
|
197
197
|
dupe-item bug that fed the merge. The catalog-fold damage and reversal were Compass USA only.
|
|
198
198
|
See [Compass USA catalog-fold reversal](../compass-usa/workflows/catalog-fold-reversal.md).
|
|
199
|
+
- **Items listing being filtered to catalog 1 (written 2026-09-01, NOT yet applied).** A migration
|
|
200
|
+
sets `TableViews.apiWhereClause = '(Items.catalogId:eq:1)'` on `Client_CompassCanada`'s shared
|
|
201
|
+
`items` view (`slug = 'items'`, id 9, recordId 21), alongside the same change in `Client_Compass`
|
|
202
|
+
and `Client_Quad`. **Canada is the harmless case today** — Canada has a **single catalog**
|
|
203
|
+
("compass", see the bullet above), so every `Client_CompassCanada.Items` row is already catalog 1 and so the filter changes nothing visible now; it only pins the listing so a
|
|
204
|
+
future non-catalog-1 item (e.g. anything a NetSuite-style sync creates) would silently not appear.
|
|
205
|
+
Mechanism: [TableView apiWhereClause row-filtering](../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md);
|
|
206
|
+
catalog background: [Compass item catalogs](../compass-usa/features/item-catalogs-and-duplicate-items.md).
|
|
199
207
|
- Related: [Compass USA](../compass-usa/profile.md).
|
|
@@ -17,6 +17,7 @@ files:
|
|
|
17
17
|
- worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php
|
|
18
18
|
related:
|
|
19
19
|
- odp-edi-850-item-resolution.md
|
|
20
|
+
- ../../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md
|
|
20
21
|
- mits-po-to-so-item-linking.md
|
|
21
22
|
- mits-sales-order-transmission-alerting.md
|
|
22
23
|
- isfulfillable-data-quality-and-type-rule.md
|
|
@@ -132,6 +133,32 @@ Plus `-1` / `-2` suffixed copies: items **2431, 2432, 2443, 2653**.
|
|
|
132
133
|
(e.g. the `#B1H` / `-S` variant forms above) or genuinely-shared ones before treating table-wide
|
|
133
134
|
`partNumber` uniqueness as settled.
|
|
134
135
|
|
|
136
|
+
## Filtering by catalog: fine for *display*, never for *resolution*
|
|
137
|
+
|
|
138
|
+
The blanket rule below ("never scope a Compass item lookup to one catalog") is about **resolving a
|
|
139
|
+
part number to a record** — order import, PO→SO linking, vendor-item matching. It does **not** mean
|
|
140
|
+
a catalog filter is banned everywhere. A **listing** is the legitimate exception: showing a user
|
|
141
|
+
only the customer-facing Compass catalog is a product decision, and nothing downstream re-resolves
|
|
142
|
+
off that list.
|
|
143
|
+
|
|
144
|
+
**In flight as of 2026-09-01 (SQL written, NOT yet applied):** the shared TOGa Supply `items` table
|
|
145
|
+
view (`TableViews.slug = 'items'`, `id 9`, `recordId 21`) is being restricted to catalog 1 by
|
|
146
|
+
setting `TableViews.apiWhereClause = '(Items.catalogId:eq:1)'` — one `UPDATE … WHERE slug = 'items'`
|
|
147
|
+
per schema, in `Client_Compass`, `Client_CompassCanada` **and** `Client_Quad` (the same view exists
|
|
148
|
+
with the same id/recordId in all three). No application code changes; the mechanism is
|
|
149
|
+
[TableView row-filtering via apiWhereClause](../../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md).
|
|
150
|
+
The migration was written as a loose `items_tableview_catalog_filter_2026-09-01.sql` in the
|
|
151
|
+
workspace root and handed to a developer to run (the DB tooling is read-only) — it still needs to
|
|
152
|
+
land in `dbchanges2/Client_<schema>/` under the mandatory `YYYY-MM-DD<letter> - Name.sql` filename
|
|
153
|
+
before it is a real change-set.
|
|
154
|
+
|
|
155
|
+
**Open decision on the clause, and it is the NULL question again.** `Items.catalogId` is nullable in
|
|
156
|
+
all three schemas, so a bare `:eq:1` silently drops the NULL-catalog items (a real, non-trivial
|
|
157
|
+
population here — see the gotcha below, and it is proportionally much larger in `Client_Quad`). If
|
|
158
|
+
those items must stay visible in the listing, the clause has to be
|
|
159
|
+
`(IFNULL(Items.catalogId,1):eq:1)` instead. Decide before applying; the two forms are not
|
|
160
|
+
interchangeable.
|
|
161
|
+
|
|
135
162
|
## "Who changed this part number?" — the audit trail says nobody
|
|
136
163
|
|
|
137
164
|
The reflex answer to a part number that looks edited is to hunt for the person who edited it. For
|
|
@@ -151,9 +178,10 @@ children" rule:
|
|
|
151
178
|
|
|
152
179
|
## Gotchas
|
|
153
180
|
|
|
154
|
-
- **⚠ Never scope a Compass item lookup to one catalog** unless you have proved the item can only
|
|
155
|
-
come from that source.
|
|
156
|
-
|
|
181
|
+
- **⚠ Never scope a Compass item *lookup* to one catalog** unless you have proved the item can only
|
|
182
|
+
come from that source. (A read-only **listing** filter is the one sanctioned exception — see
|
|
183
|
+
[Filtering by catalog](#filtering-by-catalog-fine-for-display-never-for-resolution) above.) The
|
|
184
|
+
correct default is `Items.partNumber IN (...)` with **no** catalog condition, and then reference the resolved record **by `uuid`**. **The catalog filter never buys
|
|
157
185
|
uniqueness on Compass** — `Items.partNumber` is unique across the whole table (re-verified prod
|
|
158
186
|
2026-09-01) — so every catalog condition is pure downside.
|
|
159
187
|
- **⚠ The 2.0 side hardcodes the OTHER catalog, and it broke MR purchase orders.**
|
|
@@ -193,6 +221,14 @@ children" rule:
|
|
|
193
221
|
**unresolved tension**: that reversal folded/restored 199 catalog-1 items by `UPPER(partNumber)`,
|
|
194
222
|
which implies shared cross-catalog part numbers pre-merge and does not square with the same-day
|
|
195
223
|
"partNumber is unique table-wide" verification — needs a prod re-check. (jcardinal)
|
|
224
|
+
- 2026-09-01 — Split the catalog-filter rule into **display vs. resolution**: the "never scope to one
|
|
225
|
+
catalog" ban covers part-number *resolution*, while a read-only *listing* filter is sanctioned.
|
|
226
|
+
Recorded the in-flight (written, **not yet applied**) change setting
|
|
227
|
+
`TableViews.apiWhereClause = '(Items.catalogId:eq:1)'` on the shared `items` view
|
|
228
|
+
(`slug = 'items'`, id 9, recordId 21) in `Client_Compass`, `Client_CompassCanada` and
|
|
229
|
+
`Client_Quad`, and the open decision it carries: `Items.catalogId` is nullable, so bare `:eq:1`
|
|
230
|
+
hides NULL-catalog items and `IFNULL(Items.catalogId,1):eq:1` is the alternative. Migration still
|
|
231
|
+
needs moving into `dbchanges2/Client_<schema>/`. (bala)
|
|
196
232
|
- 2026-09-01 — Recorded a **second, independent victim of the same catalog-scoping trap, on the 2.0
|
|
197
233
|
side**: `_Model_Compass_PurchaseOrder::handleMrOrder` references MR sales-order items with a
|
|
198
234
|
hardcoded `COMPASS_CATALOG_UUID` (`961dbcbc…` = catalog **1**), which is the mirror image of the
|
|
@@ -142,4 +142,16 @@ Client-specific DB change-sets live in `dbchanges2/Client_Quad/`.
|
|
|
142
142
|
Quad's **storefront only** (not orders/transmit), and Quad was the **only other client besides
|
|
143
143
|
Compass** that needed a reversal. See
|
|
144
144
|
[Quad catalog-fold reversal](./workflows/catalog-fold-reversal.md).
|
|
145
|
+
- **Items listing being filtered to catalog 1 (written 2026-09-01, NOT yet applied) — verify the
|
|
146
|
+
intent for Quad first.** A migration sets `TableViews.apiWhereClause = '(Items.catalogId:eq:1)'`
|
|
147
|
+
on `Client_Quad`'s shared `items` view (`slug = 'items'`, id 9, recordId 21); the same one-line
|
|
148
|
+
change is going to `Client_Compass` and `Client_CompassCanada`. Read it against the bullet above:
|
|
149
|
+
for Quad, catalog 1 is the **storefront (Etilize)** catalog and catalog 2 is the **procurement
|
|
150
|
+
(Agilant/NetSuite)** catalog that Quad's storefront orders actually sit on, so `:eq:1` hides the
|
|
151
|
+
procurement half of `Client_Quad.Items` from the Supply items listing — a much bigger cut than the
|
|
152
|
+
equivalent change on Compass, and a majority of Quad's items. `Items.catalogId` is also nullable
|
|
153
|
+
here and a bare `:eq:1` drops those rows too; `IFNULL(Items.catalogId,1):eq:1` recovers the NULLs
|
|
154
|
+
but not catalog 2. Mechanism:
|
|
155
|
+
[TableView apiWhereClause row-filtering](../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md);
|
|
156
|
+
catalog background: [Compass item catalogs](../compass-usa/features/item-catalogs-and-duplicate-items.md).
|
|
145
157
|
- This profile is a starting point; expand as more Quad-specific behavior is captured.
|
package/package.json
CHANGED