Repository navigation
Commit 2ce2646
feat(console): Connect an AI Assistant names the connected assistant and its last call; plain-words page subtitle (#31198)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XUajPDb8jMKTWyncZpJ2wTS)_
Fixes #30898
Clause-②: yes (widening)
Metadata-protocol: none
## 维护者速读
- 改了什么:「连接 AI 助手」页示例请求下方那一行,在本人已连接后改为说明连的是哪个助手(或哪把 API
密钥)、最近一次调用是什么时候;没连接时仍是「先试第一条」。页头副标题去掉 MCP、行级安全等协议词,改成大白话。
- 为什么改:卡 #30898 期望 2 与期望 3 的剩余部分:连接之后回到页面应能看出连接成功了;页头仍在说「支持 MCP 的 AI
客户端」。
- 风险与回滚:只读两份已有记录(本人已授权的助手、本人的 API
密钥),不新增服务端接口;读失败时退回「先试第一条」,绝不误报「未连接」。回滚即还原本 PR,无数据迁移。
-
先看:ui/packages/app-shell/src/console/connect/connectedAccess.ts(「已连接」的唯一判定与排序)、ConnectAgentWidget.tsx
的 ConnectedStatus、ui/packages/i18n/src/locales/en.ts(新增 6 个键)。
- 两分钟验证:pnpm dev 后以授权过助手或建过 API 密钥的用户打开 账户 → 连接 AI
助手,示例请求下方显示「已连接:Claude Desktop。最近一次调用在 … 起的那一小时内。」;撤销后该行回到「先试第一条」。
- 需要你做的:无,席位落地。
## What changes
- **The Connected line** (`ConnectAgentWidget.tsx`, `ConnectedStatus`).
Under the example requests, once the signed-in person has connected, the
page names what is connected and when it last made a call, the most
recent call first:
- an authorized assistant by its name ("Connected: Claude Desktop."),
with its last call "in the hour from" the recorded hour, because the
server records `lastUsedAt` as the start of an hour;
- an API key by its name ("Connected with the API key "doubao key"."),
with its last call as a moment (`last_used_at`);
- "No call yet: ask it the first request above to check the connection."
when nothing is recorded;
- one more line pointing to the list at the bottom of the page when more
than one thing is connected.
With nothing connected the line stays the try-first hint PR #31052
shipped. While the read is out the line draws nothing, so it does not
flip after a moment.
- **One "has connected" read** (`connectedAccess.ts`,
`useConnectedAccess`). It reads only the records the "What can act as
you" list already reads: `oauth.authorizedClients.list()` and the
person's own `sys_api_key` rows (filtered to `user_id`, as the list
does). A revoked key or one past `expires_at` is not a connection. A
failed read is never reported as "nothing connected": with the other
read empty the answer is `unknown` (the page shows the try-first hint),
and with the other read holding a connection that one is named. The
combining rule is the exported pure function `combineConnectedReads`.
#30895's Home card and checklist row are meant to read this same hook.
No new server route.
- **It follows the page's own Revoke.** An API-key revoke already
announces `sys_api_key` on the data bus. The assistants list's withdraw
now announces `sys_oauth_consent` (the record a grant is) after every
withdraw, answered or refused, so the Connected line reads again.
- `accessRecords.ts` (new): the `sys_api_key` columns, the `ApiKeyRow` /
`AuthorizedClient` types and the expiry rule, moved out of
`AuthorizedAccessList.tsx` so the list and the hook share them (the list
keeps exporting components only, so it adds no react-refresh warning).
- **Page subtitle** (Expected 3 leftover, wording only):
`packages/mcp/src/connect-ui.ts` now reads "Work in this system from
Claude, Doubao and other AI assistants. Each one acts as you: it sees
and changes only what your own permissions allow."
`pages.connect_agent.subtitle` follows in en, es-ES, ja-JP, zh-CN and
zh-Hant, and the four translated locales' source digests are re-recorded
(`collectSourceHashes(en)` gives `51c467af44024a1b`).
- **Language packs**, all eleven: `connectAgent.capabilities.connected`,
`connectedKey`, `lastCallHour`, `lastCallAt`, `noCallYet`,
`moreConnected` added; no existing key reworded or removed. `zh-Hant`
regenerated from `zh-Hans` with `gen:zh-hant`.
- Changeset `.changeset/30898-connect-agent-connected-line.md`
(`@object-ui/app-shell`, `@object-ui/i18n`, `@objectstack/mcp`,
`@objectstack/platform-objects`: patch).
## Why
The card's Expected 2: "Once the person has connected, the page says so:
which assistant, and when it last made a call. With nothing connected
yet, it says what to try first." It waited on #30894, which closed today
with the authorized-assistants read and Revoke. Expected 3 left the
page-header subtitle saying "MCP-capable AI client" and "row-level
security".
## Tests
- `ConnectAgentWidget.connected-30898.test.tsx`, 14 cases, rendering the
whole page and driving the real controls: §1 nothing connected (no
grants, only a revoked and an expired key) keeps the try-first line and
reads only the person's own keys; §2 an assistant is named with its
hour, a nameless one by the fallback, "no call yet" when none is
recorded; §3 the most recent call wins (a key that called after every
assistant is named as an API key at its moment; a revoked key that
called later still is not named; more than one points to the list); §4 a
failed read falls back to the try-first line, or still names what the
other read found; §5 clicking Revoke and confirming, on the only
assistant and on the only key, turns the line back to the try-first
hint; §6 `combineConnectedReads` on its own (loading, unknown, not
connected only when both answered empty, ordering).
- `ConnectAgentWidget.keyName-13355.test.tsx`: the capability-key list
pin gains the six keys, so every pack must carry each of them and none
may be an English copy.
- `de-quote-pairing-3876.test.ts`: 109 → 110 paired „…“ spans, for
`connectAgent.capabilities.connectedKey` („{{name}}“), recorded in the
pin's history comment as earlier additions were.
- Ablations (one-off, `scripts/ablation-replace.mjs`, restored blob
equal to HEAD each time): deleting the withdraw's `notifyDataChanged({
objectName: 'sys_oauth_consent', … })` turned §5's assistant case red (1
failed, 13 passed); replacing the dead-key filter with
`rows.map(keyEntry)` turned six cases red (6 failed, 8 passed).
## Acceptance notes
- **"Last call" exists on both records**, so nothing is invented:
`lastUsedAt` on an authorized assistant (the start of the hour, which is
why the line says "in the hour from") and `last_used_at` on an API key.
- **An API key counts as a connection.** The card names "an
authorization or an API key the person holds" for #30895's checklist
row, and the order reads both records; a key is named as an API key, so
a CI key reads as what it is.
- **The page now reads both lists twice** (the Connected line and the
list at the bottom each read them once). The list was left as PR #31084
shipped it; the cost is one more `GET /auth/authorized-clients` and one
more `sys_api_key` find on this page.
- `Clause-②: yes (widening)` is the seat's measured arm for new
catalogue keys in the published `@object-ui/i18n`; no key is removed.
## Verification
<details><summary>Live, showcase on this branch (Vite console against
it), admin and member, en and zh</summary>
- Administrator, en, after minting "doubao key" and calling `GET
/api/v1/meta/objects` with it (200): page header "Work in this system
from Claude, Doubao and other AI assistants. Each one acts as you: it
sees and changes only what your own permissions allow."; Connected line
"Connected with the API key "doubao key". Its last call was at Nov 7,
2026, 4:22 PM."
- Administrator, zh: header 「在 Claude、豆包等 AI
助手里使用本系统。助手始终以你的身份操作:只能查看和修改你有权限的内容。」; line 「已用 API 密钥“doubao
key”连接。最近一次调用在 2026年11月7日 16:22。」
- Member (created with `admin/create-user`, password changed), en,
nothing connected: no Connected line; the try-first line is shown.
- Member with one grant: the `sys_oauth_application` and
`sys_oauth_consent` rows were written straight into the dev database
(Claude Desktop, `last_used_at` 16:00), not through an OAuth flow; `GET
/auth/authorized-clients` then answered that grant. zh: 「已连接:Claude
Desktop。最近一次调用在 2026年11月7日 16:00 起的那一小时内。」 en, then Revoke on its row
and confirm in the dialog: the Connected line left, the try-first line
came back, and `GET /auth/authorized-clients` answered `[]`.
</details>
<details><summary>Local tests and gates</summary>
- `pnpm --dir ui exec vitest run packages/app-shell/src/console/connect/
packages/i18n/
packages/app-shell/src/__tests__/product-neutral-renderer.ratchet.test.ts`:
102 files passed, 1832 tests passed, 13 skipped (head a93d7f2; later
commits touch only the changeset).
- `pnpm --filter @object-ui/app-shell run type-check` and
`@object-ui/i18n` type-check: exit 0. eslint on `console/connect/`: 0
errors, 3 warnings (all existing, in `ConnectAgentWidget.tsx`'s
untouched `any`s). ui `check:new-line-citations`: 0 new.
- `pnpm --filter @objectstack/platform-objects test`: 119 files, 1656
passed. `pnpm --filter @objectstack/mcp test`: 60 files, 767 passed.
- `node scripts/pm/dispatch-gates.mjs --commands` derived 66 families;
all 66 exit 0; `--ran` reconciliation: 66 derived, 66 run, 0
NOT-MEASURED, 0 UNRUN. Two needed a second run: `pnpm --dir ui exec
vitest run scripts/` was SIGKILLed by the box's memory limit on the
first pass and then passed (170 files, 2 skipped); `pnpm
check:dual-build-cjs-loads` answered exit 3 PREREQUISITE NOT MET (seven
packages without dist) until they were built, then exit 0. The changeset
families and `check:nul-bytes` were re-run on the final head 7005f76,
all exit 0.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(No spec change.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; eslint on the touched files passes (the
repo-wide `pnpm lint` is CI's).
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: n/a.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01FEkQ5pMhSEyeAKtt6btsBe
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5pMhSEyeAKtt6btsBe)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 30002df commit 2ce2646
29 files changed
Lines changed: 688 additions & 55 deletions
File tree
- .changeset
- packages
- mcp/src
- platform-objects/src/apps/translations
- ui/packages
- app-shell/src/console/connect
- i18n/src
- __tests__
- locales
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
74 | 74 | | |
75 | 75 | | |
76 | 76 | | |
77 | | - | |
78 | | - | |
| 77 | + | |
| 78 | + | |
79 | 79 | | |
80 | 80 | | |
81 | 81 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
290 | 290 | | |
291 | 291 | | |
292 | 292 | | |
293 | | - | |
294 | | - | |
| 293 | + | |
| 294 | + | |
295 | 295 | | |
296 | 296 | | |
297 | 297 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
182 | 182 | | |
183 | 183 | | |
184 | 184 | | |
185 | | - | |
| 185 | + | |
186 | 186 | | |
187 | 187 | | |
188 | 188 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
191 | 191 | | |
192 | 192 | | |
193 | 193 | | |
194 | | - | |
195 | | - | |
| 194 | + | |
| 195 | + | |
196 | 196 | | |
197 | 197 | | |
198 | 198 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
182 | 182 | | |
183 | 183 | | |
184 | 184 | | |
185 | | - | |
| 185 | + | |
186 | 186 | | |
187 | 187 | | |
188 | 188 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
190 | 190 | | |
191 | 191 | | |
192 | 192 | | |
193 | | - | |
| 193 | + | |
194 | 194 | | |
195 | 195 | | |
196 | 196 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
182 | 182 | | |
183 | 183 | | |
184 | 184 | | |
185 | | - | |
| 185 | + | |
186 | 186 | | |
187 | 187 | | |
188 | 188 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
195 | 195 | | |
196 | 196 | | |
197 | 197 | | |
198 | | - | |
| 198 | + | |
199 | 199 | | |
200 | 200 | | |
201 | 201 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
190 | 190 | | |
191 | 191 | | |
192 | 192 | | |
193 | | - | |
| 193 | + | |
194 | 194 | | |
195 | 195 | | |
196 | 196 | | |
0 commit comments