@agent-native/core 0.161.23 → 0.163.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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/analytics/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +18 -2
- package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/assets/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/brain/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/calendar/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/calendar/server/handlers/google-auth.ts +1 -8
- package/corpus/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +88 -35
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +36 -1
- package/corpus/templates/clips/app/i18n/en-US.ts +4 -3
- package/corpus/templates/clips/app/lib/camera-composite.ts +15 -1
- package/corpus/templates/clips/changelog/2026-08-18-clips-no-longer-crashes-when-a-camera-bubble-starts-at-a-tin.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +30 -40
- package/corpus/templates/clips/desktop/src/components/MicOffConfirmation.tsx +47 -0
- package/corpus/templates/clips/desktop/src/styles.css +110 -0
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +114 -7
- package/corpus/templates/clips/shared/recording-capture.ts +18 -13
- package/corpus/templates/content/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/content/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/content/actions/create-content-database.ts +1 -1
- package/corpus/templates/content/actions/create-document.ts +1 -1
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +3 -5
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +0 -3
- package/corpus/templates/content/app/components/editor/NotionConflictBanner.tsx +0 -4
- package/corpus/templates/content/app/components/editor/NotionSyncBar.tsx +4 -1
- package/corpus/templates/content/app/hooks/use-notion.ts +17 -15
- package/corpus/templates/crm/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/design/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/design/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +6 -1
- package/corpus/templates/dispatch/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/dispatch/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/factory/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/factory/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/forms/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/macros/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/macros/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/mail/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +18 -3
- package/corpus/templates/mail/server/handlers/google-auth.ts +1 -8
- package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/plan/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +4 -0
- package/corpus/templates/slides/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/slides/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/slides/actions/_await-fit-check.ts +30 -5
- package/corpus/templates/slides/actions/add-slide.ts +6 -2
- package/corpus/templates/slides/actions/create-deck.ts +2 -1
- package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +3 -3
- package/corpus/templates/slides/app/components/editor/GoogleSlidesReferenceImport.tsx +3 -3
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +3 -1
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +1 -1
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +1 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +35 -2
- package/corpus/templates/slides/app/routes/p.$id.tsx +3 -1
- package/corpus/templates/slides/changelog/2026-08-18-generated-slides-preserve-speaker-notes.md +6 -0
- package/corpus/templates/slides/server/handlers/assets.ts +23 -12
- package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +15 -6
- package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +15 -6
- package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +34 -19
- package/corpus/templates/slides/server/handlers/request-auth-context.ts +1 -0
- package/corpus/templates/slides/server/handlers/share.ts +1 -1
- package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/slides/server/routes/api/deck-agent-context.json.get.ts +1 -1
- package/corpus/templates/slides/server/routes/api/exports/[filename].get.ts +9 -3
- package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +12 -5
- package/corpus/templates/slides/server/routes/api/exports/html.post.ts +9 -7
- package/corpus/templates/slides/server/routes/api/exports/pptx.post.ts +9 -7
- package/corpus/templates/slides/shared/api.ts +7 -1
- package/corpus/templates/tasks/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/tasks/.agents/skills/sharing/SKILL.md +27 -2
- package/dist/agent/engine/translate-anthropic.js +27 -2
- package/dist/agent/production-agent.d.ts +2 -2
- package/dist/agent/production-agent.js +16 -8
- package/dist/agent/thread-data-builder.d.ts +1 -0
- package/dist/agent/thread-data-builder.js +6 -4
- package/dist/agent/tool-approval-store.d.ts +10 -1
- package/dist/agent/tool-approval-store.js +19 -2
- package/dist/agent/types.d.ts +9 -0
- package/dist/app-config/schema.d.ts +1 -0
- package/dist/app-config/workspace.d.ts +1 -0
- package/dist/app-config/workspace.js +8 -0
- package/dist/chat-threads/schema.d.ts +2 -2
- package/dist/client/AssistantChat.js +14 -6
- package/dist/client/EnvironmentBadge.d.ts +15 -0
- package/dist/client/EnvironmentBadge.js +79 -0
- package/dist/client/MultiTabAssistantChat.js +73 -17
- package/dist/client/agent-chat-adapter.js +11 -1
- package/dist/client/api-path.js +14 -1
- package/dist/client/app-providers.js +2 -1
- package/dist/client/chat/tool-call-display.d.ts +12 -2
- package/dist/client/chat/tool-call-display.js +17 -7
- package/dist/client/chat/widgets/WorkspaceFileWidget.js +1 -1
- package/dist/client/chat/widgets/builtin-tool-renderers.d.ts +6 -0
- package/dist/client/chat/widgets/builtin-tool-renderers.js +19 -0
- package/dist/client/onboarding/FirstRunOnboarding.js +46 -32
- package/dist/client/onboarding/use-onboarding.d.ts +8 -1
- package/dist/client/onboarding/use-onboarding.js +22 -7
- package/dist/client/org/TeamPage.js +15 -2
- package/dist/client/org/hooks.d.ts +2 -0
- package/dist/client/org/hooks.js +12 -0
- package/dist/client/org/index.d.ts +2 -2
- package/dist/client/org/index.js +1 -1
- package/dist/client/session-replay.js +58 -15
- package/dist/client/settings/SettingsTabsPage.js +19 -6
- package/dist/client/sharing/ShareButton.js +43 -18
- package/dist/client/sharing/useShareButtonController.d.ts +12 -2
- package/dist/client/sharing/useShareButtonController.js +38 -8
- package/dist/client/sse-event-processor.d.ts +7 -0
- package/dist/client/sse-event-processor.js +4 -1
- package/dist/client/ui/index.d.ts +1 -0
- package/dist/client/ui/index.js +1 -0
- package/dist/client/use-action.js +26 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/config.d.ts +15 -0
- package/dist/config.js +53 -0
- package/dist/dashboard-storage/schema.d.ts +2 -2
- package/dist/data-programs/schema.d.ts +2 -2
- package/dist/extensions/fetch-tool.js +8 -2
- package/dist/extensions/schema.d.ts +2 -2
- package/dist/extensions/store.js +3 -0
- package/dist/localization/core-messages/ar-SA.js +1 -0
- package/dist/localization/core-messages/de-DE.js +1 -0
- package/dist/localization/core-messages/en-US.d.ts +1 -0
- package/dist/localization/core-messages/en-US.js +1 -0
- package/dist/localization/core-messages/es-ES.js +1 -0
- package/dist/localization/core-messages/fr-FR.js +1 -0
- package/dist/localization/core-messages/hi-IN.js +1 -0
- package/dist/localization/core-messages/ja-JP.js +1 -0
- package/dist/localization/core-messages/ko-KR.js +1 -0
- package/dist/localization/core-messages/pt-BR.js +1 -0
- package/dist/localization/core-messages/zh-CN.js +1 -0
- package/dist/localization/core-messages/zh-TW.js +1 -0
- package/dist/localization/core-messages.d.ts +1 -0
- package/dist/localization/default-messages.d.ts +4 -0
- package/dist/localization/default-messages.js +4 -0
- package/dist/mcp/embed-app.js +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/org/handlers.d.ts +6 -1
- package/dist/org/handlers.js +43 -0
- package/dist/org/index.d.ts +3 -3
- package/dist/org/index.js +2 -2
- package/dist/org/migrations.js +33 -0
- package/dist/org/plugin.js +10 -1
- package/dist/org/schema.d.ts +315 -0
- package/dist/org/schema.js +20 -1
- package/dist/org/types.d.ts +3 -0
- package/dist/org/workspace-app-access.d.ts +10 -0
- package/dist/org/workspace-app-access.js +179 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +7 -7
- package/dist/provider-api/index.d.ts +10 -0
- package/dist/provider-api/index.js +45 -9
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.js +24 -4
- package/dist/server/poll.js +13 -3
- package/dist/server/sentry.d.ts +1 -1
- package/dist/sharing/access.d.ts +2 -2
- package/dist/sharing/access.js +56 -3
- package/dist/sharing/actions/list-resource-shares.d.ts +2 -0
- package/dist/sharing/actions/list-resource-shares.js +26 -1
- package/dist/sharing/actions/share-resource.d.ts +1 -1
- package/dist/sharing/actions/share-resource.js +23 -4
- package/dist/sharing/actions/unshare-resource.d.ts +1 -1
- package/dist/sharing/actions/unshare-resource.js +1 -1
- package/dist/sharing/registry.d.ts +12 -0
- package/dist/sharing/schema.d.ts +4 -4
- package/dist/sharing/schema.js +2 -2
- package/dist/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
- package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/templates/default/.agents/skills/sharing/SKILL.md +27 -2
- package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +27 -2
- package/dist/vite/client.d.ts +5 -0
- package/dist/vite/client.js +64 -2
- package/dist/workspace-connections/actions/list-workspace-user-groups.d.ts +9 -1
- package/dist/workspace-connections/actions/list-workspace-user-groups.js +9 -4
- package/dist/workspace-connections/groups.d.ts +2 -0
- package/dist/workspace-connections/groups.js +16 -5
- package/dist/workspace-files/actions.d.ts +1 -8
- package/dist/workspace-files/actions.js +5 -21
- package/dist/workspace-files/index.d.ts +1 -1
- package/dist/workspace-files/index.js +1 -1
- package/dist/workspace-files/store.d.ts +18 -0
- package/dist/workspace-files/store.js +24 -3
- package/package.json +3 -3
- package/src/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/src/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
- package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/src/templates/default/.agents/skills/sharing/SKILL.md +27 -2
- package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +27 -2
|
@@ -18,6 +18,11 @@ Any resource a user **creates** (dashboards, documents, forms, decks, compositio
|
|
|
18
18
|
|
|
19
19
|
This is the framework-level primitive. Every ownable resource gets it for free — same API, same UI, same skill.
|
|
20
20
|
|
|
21
|
+
Workspace apps are the intentional organization-scoped exception: their default
|
|
22
|
+
visibility is `org`, configurable by an organization owner/admin. The creator is
|
|
23
|
+
stored as the app owner, and the creator or an organization owner/admin can
|
|
24
|
+
change the app's visibility or manage its share grants.
|
|
25
|
+
|
|
21
26
|
## Concepts
|
|
22
27
|
|
|
23
28
|
### Three visibility levels
|
|
@@ -26,7 +31,8 @@ This is the framework-level primitive. Every ownable resource gets it for free
|
|
|
26
31
|
- **`org`** — owner + explicit grants + anyone in the same org (read-only).
|
|
27
32
|
- **`public`** — owner + explicit grants + **anyone with the link** (read-only). Public docs do NOT appear in other users' list/sidebar/search results — `accessFilter` omits them by default. They're reachable by id (`resolveAccess` admits them) so direct links and SSR routes like `/p/:id` keep working. If a list endpoint legitimately needs cross-user public discovery (a template gallery, etc.), pass `accessFilter(table, shares, ctx, minRole, { includePublic: true })`.
|
|
28
33
|
|
|
29
|
-
Visibility is coarse. Explicit share grants are fine-grained (per user
|
|
34
|
+
Visibility is coarse. Explicit share grants are fine-grained (per user, reusable
|
|
35
|
+
organization group, or per org).
|
|
30
36
|
|
|
31
37
|
### Roles on a share grant
|
|
32
38
|
|
|
@@ -101,6 +107,7 @@ registerShareableResource({
|
|
|
101
107
|
// ...
|
|
102
108
|
allowPublic: false, // hides "Public" in the share dialog and rejects it server-side
|
|
103
109
|
requireOrgMemberForUserShares: true, // user shares must target an org member or pending invitee
|
|
110
|
+
supportsGroupShares: true, // enable reusable organization groups for this resource
|
|
104
111
|
});
|
|
105
112
|
```
|
|
106
113
|
|
|
@@ -111,6 +118,21 @@ Use both for resources that execute code or expose privileged data with the *vie
|
|
|
111
118
|
|
|
112
119
|
Defaults match historical behaviour: `allowPublic: true`, `requireOrgMemberForUserShares: false`. Resources that don't set the flags work as before.
|
|
113
120
|
|
|
121
|
+
### Reusable organization groups
|
|
122
|
+
|
|
123
|
+
The workspace organization model stores reusable groups in
|
|
124
|
+
`workspace_user_groups`. Organization owners/admins manage the groups and their
|
|
125
|
+
members through the existing group actions. Set `supportsGroupShares: true` on a
|
|
126
|
+
resource registration to enable `principalType: "group"` in the generic share
|
|
127
|
+
actions and the shared popover's autocomplete. Group ids are always checked
|
|
128
|
+
against the resource's organization, and membership is evaluated at access time
|
|
129
|
+
so group changes apply to existing shares.
|
|
130
|
+
|
|
131
|
+
Workspace apps opt into this flag and use the organization default visibility
|
|
132
|
+
setting. The app creator is the durable `owner_email`, while organization
|
|
133
|
+
owners/admins receive admin access through the registration's
|
|
134
|
+
`canManageAccess` callback.
|
|
135
|
+
|
|
114
136
|
## Filter list/read queries
|
|
115
137
|
|
|
116
138
|
```ts
|
|
@@ -197,6 +219,9 @@ All app share popovers should use the same compact surface contract:
|
|
|
197
219
|
- Add `ShareAgentsSection` only when the resource has a real agent-readable
|
|
198
220
|
link or prompt. Keep it collapsed by default and supply domain-specific
|
|
199
221
|
content through the shared section shell.
|
|
222
|
+
- When `supportsGroupShares` is enabled, keep the existing people input as the
|
|
223
|
+
only entry point and include organization groups in its autocomplete. Do not
|
|
224
|
+
add a separate group-management surface to each resource.
|
|
200
225
|
- App-specific tabs or controls may remain when they represent a real domain
|
|
201
226
|
action (for example, an embed-code editor), but they should retain the same
|
|
202
227
|
trigger, copy-row, access, and spacing language.
|
|
@@ -211,7 +236,7 @@ The framework auto-mounts these actions in every template — no per-template bo
|
|
|
211
236
|
|
|
212
237
|
| Action | Args | Purpose |
|
|
213
238
|
| -------------------------- | ------------------------------------------------------------------------------ | ----------------------------------------- |
|
|
214
|
-
| `share-resource` | `resourceType, resourceId, principalType, principalId, role, notify?, resourceUrl?, message?` | Grant a user or org access. `notify` defaults to true for individual user shares; `resourceUrl` can provide the direct app link and `message` an optional short note for the notification email. |
|
|
239
|
+
| `share-resource` | `resourceType, resourceId, principalType, principalId, role, notify?, resourceUrl?, message?` | Grant a user, group, or org access. `notify` defaults to true for individual user shares; `resourceUrl` can provide the direct app link and `message` an optional short note for the notification email. |
|
|
215
240
|
| `unshare-resource` | `resourceType, resourceId, principalType, principalId` | Revoke access. |
|
|
216
241
|
| `list-resource-shares` | `resourceType, resourceId` | Current visibility + all share grants. |
|
|
217
242
|
| `set-resource-visibility` | `resourceType, resourceId, visibility` | Change to `private` / `org` / `public`. |
|
|
@@ -261,17 +261,42 @@ export function backfillEngineMessagesToolResults(messages) {
|
|
|
261
261
|
// ---------------------------------------------------------------------------
|
|
262
262
|
// EngineMessage → Anthropic.MessageParam
|
|
263
263
|
// ---------------------------------------------------------------------------
|
|
264
|
+
/**
|
|
265
|
+
* A thinking block replays only with the signature Anthropic issued for it, or
|
|
266
|
+
* (redacted) with its encrypted payload. An empty signature is not a fallback:
|
|
267
|
+
* the native API rejects the whole request, so a turn that streamed fine dies
|
|
268
|
+
* with a provider error pointing nowhere near the cause. Drop the unsendable
|
|
269
|
+
* block and say so instead of coercing it into a guaranteed 400.
|
|
270
|
+
*
|
|
271
|
+
* Not applied to the Builder gateway: its tolerance for an unsigned thinking
|
|
272
|
+
* block is unverified, so that path keeps its existing behavior rather than
|
|
273
|
+
* trading a working request for an untested one.
|
|
274
|
+
*/
|
|
275
|
+
function replayableAnthropicPart(part) {
|
|
276
|
+
if (part.type !== "thinking")
|
|
277
|
+
return true;
|
|
278
|
+
if (part.redactedData || part.signature)
|
|
279
|
+
return true;
|
|
280
|
+
console.warn("[anthropic-engine] dropping a thinking block with no signature; it cannot be replayed");
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
264
283
|
export function engineMessageToAnthropic(msg, opts) {
|
|
265
284
|
const builderGateway = opts?.builderGateway === true;
|
|
285
|
+
const content = builderGateway
|
|
286
|
+
? msg.content
|
|
287
|
+
: msg.content.filter(replayableAnthropicPart);
|
|
266
288
|
return {
|
|
267
289
|
role: msg.role,
|
|
268
|
-
content:
|
|
290
|
+
content: content.map((p) => enginePartToAnthropic(p, builderGateway)),
|
|
269
291
|
};
|
|
270
292
|
}
|
|
271
293
|
/** Messages for the Anthropic HTTP API (strict schema — no extra tool_result fields). */
|
|
272
294
|
export function engineMessagesToAnthropic(messages) {
|
|
273
295
|
const normalized = backfillEngineMessagesToolResults(messages);
|
|
274
|
-
return normalized.
|
|
296
|
+
return normalized.flatMap((m) => {
|
|
297
|
+
const translated = engineMessageToAnthropic(m);
|
|
298
|
+
return translated.content.length > 0 ? [translated] : [];
|
|
299
|
+
});
|
|
275
300
|
}
|
|
276
301
|
/**
|
|
277
302
|
* Messages for the Builder LLM gateway (Gemini-backed). Same Anthropic-shaped
|
|
@@ -700,7 +700,7 @@ export interface ExecuteAgentToolCallOptions {
|
|
|
700
700
|
threadId?: string;
|
|
701
701
|
turnId?: string;
|
|
702
702
|
approvedToolCalls?: string[];
|
|
703
|
-
onApprovalRequired?: (binding: AgentApprovalBinding) => Promise<void>;
|
|
703
|
+
onApprovalRequired?: (binding: AgentApprovalBinding) => Promise<string | void>;
|
|
704
704
|
consumeApproval?: (binding: AgentApprovalBinding) => Promise<boolean>;
|
|
705
705
|
send?: (event: AgentChatEvent) => void;
|
|
706
706
|
}
|
|
@@ -877,7 +877,7 @@ export declare function runAgentLoop(opts: {
|
|
|
877
877
|
* so client-supplied history is never the authorization source; direct loop
|
|
878
878
|
* callers may omit them when they own the surrounding approval boundary.
|
|
879
879
|
*/
|
|
880
|
-
onApprovalRequired?: (binding: AgentApprovalBinding) => Promise<void>;
|
|
880
|
+
onApprovalRequired?: (binding: AgentApprovalBinding) => Promise<string | void>;
|
|
881
881
|
consumeApproval?: (binding: AgentApprovalBinding) => Promise<boolean>;
|
|
882
882
|
/**
|
|
883
883
|
* In-loop processor seam (see `processors.ts`). Each processor can observe
|
|
@@ -4331,7 +4331,14 @@ export async function runAgentLoop(opts) {
|
|
|
4331
4331
|
mustApprove = true;
|
|
4332
4332
|
}
|
|
4333
4333
|
if (mustApprove) {
|
|
4334
|
-
|
|
4334
|
+
// `askId` identifies THIS gate hit, distinct from any earlier ask
|
|
4335
|
+
// for the same approvalKey/toolCallId. A failed resume (expired
|
|
4336
|
+
// grant, turn-id mismatch) re-enters this branch and mints a new
|
|
4337
|
+
// askId, which is how the client tells "an already-approved card
|
|
4338
|
+
// re-rendered" apart from "a fresh ask after the grant didn't land"
|
|
4339
|
+
// — without it, a stale client-side "approved" mark permanently
|
|
4340
|
+
// hides Approve/Deny with no way to retry.
|
|
4341
|
+
const askId = (await opts.onApprovalRequired?.(approvalBinding)) || undefined;
|
|
4335
4342
|
send({
|
|
4336
4343
|
type: "tool_start",
|
|
4337
4344
|
id: toolCall.id,
|
|
@@ -4343,6 +4350,7 @@ export async function runAgentLoop(opts) {
|
|
|
4343
4350
|
tool: toolCall.name,
|
|
4344
4351
|
input: toolCall.input,
|
|
4345
4352
|
approvalKey,
|
|
4353
|
+
...(askId ? { askId } : {}),
|
|
4346
4354
|
...(toolCall.id ? { toolCallId: toolCall.id } : {}),
|
|
4347
4355
|
});
|
|
4348
4356
|
// Audit the blocked attempt: the action did NOT run, but "the agent
|
|
@@ -7194,7 +7202,7 @@ export function createProductionAgentHandler(options) {
|
|
|
7194
7202
|
};
|
|
7195
7203
|
const approvalHooks = {
|
|
7196
7204
|
onApprovalRequired: async (binding) => {
|
|
7197
|
-
|
|
7205
|
+
return createAgentToolApproval(approvalStoreBinding(binding));
|
|
7198
7206
|
},
|
|
7199
7207
|
consumeApproval: async (binding) => {
|
|
7200
7208
|
if (!ownerEmail)
|
|
@@ -7234,12 +7242,12 @@ export function createProductionAgentHandler(options) {
|
|
|
7234
7242
|
const { threadDataToEngineMessages } = await import("./thread-data-builder.js");
|
|
7235
7243
|
const priorThreadData = (await getThread(effectiveThreadId))
|
|
7236
7244
|
?.threadData;
|
|
7237
|
-
//
|
|
7238
|
-
//
|
|
7239
|
-
//
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7245
|
+
// Replay what earlier chunks DID, not just what they said: the default
|
|
7246
|
+
// text-only rebuild drops every tool call and result, so the next chunk
|
|
7247
|
+
// re-runs work already committed and cannot see its output.
|
|
7248
|
+
const resumed = threadDataToEngineMessages(priorThreadData, {
|
|
7249
|
+
includeToolCalls: true,
|
|
7250
|
+
});
|
|
7243
7251
|
if (resumed.length > 0) {
|
|
7244
7252
|
const actionPreparationTool = typeof backgroundRunMarker?.actionPreparationTool === "string" &&
|
|
7245
7253
|
backgroundRunMarker.actionPreparationTool.trim()
|
|
@@ -144,7 +144,10 @@ export function buildAssistantMessage(events, runId, options = {}) {
|
|
|
144
144
|
const matchingIndex = findApprovalToolCallIndex(content, event.tool ?? "unknown", event.toolCallId);
|
|
145
145
|
const part = content[matchingIndex];
|
|
146
146
|
if (part?.type === "tool-call") {
|
|
147
|
-
part.approval = {
|
|
147
|
+
part.approval = {
|
|
148
|
+
approvalKey: event.approvalKey,
|
|
149
|
+
...(event.askId ? { askId: event.askId } : {}),
|
|
150
|
+
};
|
|
148
151
|
}
|
|
149
152
|
continue;
|
|
150
153
|
}
|
|
@@ -744,14 +747,13 @@ function assistantReplayContent(message, text) {
|
|
|
744
747
|
? part.args
|
|
745
748
|
: {};
|
|
746
749
|
assistant.push({ type: "tool-call", id, name, input });
|
|
747
|
-
|
|
748
|
-
continue;
|
|
750
|
+
const result = part.result === undefined ? INTERRUPTED_TOOL_RESULT : part.result;
|
|
749
751
|
results.push({
|
|
750
752
|
type: "tool-result",
|
|
751
753
|
toolCallId: id,
|
|
752
754
|
toolName: name,
|
|
753
755
|
toolInput: stringifyToolUseInputForGateway(input),
|
|
754
|
-
content: replayedToolResultContent(
|
|
756
|
+
content: replayedToolResultContent(result),
|
|
755
757
|
...(part.isError === true ? { isError: true } : {}),
|
|
756
758
|
});
|
|
757
759
|
}
|
|
@@ -21,7 +21,16 @@ export declare function resolveAgentToolApprovalTurnId(binding: {
|
|
|
21
21
|
requestedTurnId?: string | null;
|
|
22
22
|
approvalKeys: readonly string[];
|
|
23
23
|
}): Promise<string | null>;
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Creates the durable pending-approval row and returns its id as the "ask"
|
|
26
|
+
* identity for this specific gate hit. Every call is a fresh row (a fresh
|
|
27
|
+
* id), including a second gate hit for the same `approvalKey` after a prior
|
|
28
|
+
* grant failed to be consumed — so the id doubles as the signal the client
|
|
29
|
+
* needs to tell "this approval_required is the one I already approved,
|
|
30
|
+
* re-rendered" apart from "this is a NEW ask for the same tool call". See
|
|
31
|
+
* `ApprovalAffordance` in client/chat/tool-call-display.tsx.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createAgentToolApproval(binding: AgentToolApprovalBinding): Promise<string>;
|
|
25
34
|
/**
|
|
26
35
|
* Atomically consume the server-created approval for one logical tool call.
|
|
27
36
|
* The model's call id is transport metadata and can change when Dispatch
|
|
@@ -2,7 +2,13 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { getDbExec, isPostgres, retryOnDdlRace } from "../db/client.js";
|
|
3
3
|
import { ensureIndexExists, ensureTableExists } from "../db/ddl-guard.js";
|
|
4
4
|
import { AGENT_TOOL_APPROVAL_INDEX_SQL, AGENT_TOOL_APPROVAL_LOGICAL_INDEX_SQL, AGENT_TOOL_APPROVAL_RECOVERY_INDEX_SQL, AGENT_TOOL_APPROVAL_TABLE_SQL, } from "./tool-approval-migrations.js";
|
|
5
|
-
|
|
5
|
+
// 15 minutes was too short in practice: a user who steps away mid-approval
|
|
6
|
+
// (switching tabs to update their client, checking something else) comes back
|
|
7
|
+
// to a silently expired grant with no visible error — clicking Approve just
|
|
8
|
+
// does nothing, because `consumeAgentToolApproval`'s `expires_at > ?` no
|
|
9
|
+
// longer matches. An hour gives real human latency room while still bounding
|
|
10
|
+
// how long a stale grant can be replayed.
|
|
11
|
+
const APPROVAL_TTL_MS = 60 * 60_000;
|
|
6
12
|
const APPROVAL_CLEANUP_AGE_MS = 24 * 60 * 60_000;
|
|
7
13
|
let initPromise;
|
|
8
14
|
async function ensureAgentToolApprovalTable() {
|
|
@@ -74,17 +80,27 @@ export async function resolveAgentToolApprovalTurnId(binding) {
|
|
|
74
80
|
return requestedTurnId;
|
|
75
81
|
return turnIds.size === 1 ? [...turnIds][0] : null;
|
|
76
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Creates the durable pending-approval row and returns its id as the "ask"
|
|
85
|
+
* identity for this specific gate hit. Every call is a fresh row (a fresh
|
|
86
|
+
* id), including a second gate hit for the same `approvalKey` after a prior
|
|
87
|
+
* grant failed to be consumed — so the id doubles as the signal the client
|
|
88
|
+
* needs to tell "this approval_required is the one I already approved,
|
|
89
|
+
* re-rendered" apart from "this is a NEW ask for the same tool call". See
|
|
90
|
+
* `ApprovalAffordance` in client/chat/tool-call-display.tsx.
|
|
91
|
+
*/
|
|
77
92
|
export async function createAgentToolApproval(binding) {
|
|
78
93
|
await ensureAgentToolApprovalTable();
|
|
79
94
|
const now = Date.now();
|
|
80
95
|
const expiresAt = binding.expiresAt ?? now + APPROVAL_TTL_MS;
|
|
96
|
+
const id = `agent-tool-approval-${randomUUID()}`;
|
|
81
97
|
await getDbExec().execute({
|
|
82
98
|
sql: `INSERT INTO agent_tool_approvals
|
|
83
99
|
(id, owner_email, org_id, thread_id, turn_id, tool_name, call_id,
|
|
84
100
|
approval_key_hash, status, expires_at, consumed_at, created_at, updated_at)
|
|
85
101
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, NULL, ?, ?)`,
|
|
86
102
|
args: [
|
|
87
|
-
|
|
103
|
+
id,
|
|
88
104
|
binding.ownerEmail,
|
|
89
105
|
binding.orgId ?? null,
|
|
90
106
|
binding.threadId ?? null,
|
|
@@ -110,6 +126,7 @@ export async function createAgentToolApproval(binding) {
|
|
|
110
126
|
catch (error) {
|
|
111
127
|
console.warn("[agent] Could not clean expired tool approval records", error);
|
|
112
128
|
}
|
|
129
|
+
return id;
|
|
113
130
|
}
|
|
114
131
|
/**
|
|
115
132
|
* Atomically consume the server-created approval for one logical tool call.
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -305,6 +305,15 @@ export type AgentChatEvent = {
|
|
|
305
305
|
approvalKey: string;
|
|
306
306
|
/** The model-side tool-call id for this paused call, when available. */
|
|
307
307
|
toolCallId?: string;
|
|
308
|
+
/**
|
|
309
|
+
* Identifies THIS gate hit, distinct from any earlier ask carrying the
|
|
310
|
+
* same `approvalKey`/`toolCallId`. A failed resume (expired grant,
|
|
311
|
+
* turn-id mismatch) re-emits `approval_required` for the same call with
|
|
312
|
+
* a fresh `askId`; the client uses the change to tell that apart from
|
|
313
|
+
* the same ask simply re-rendering, so a stale "approved" mark can't
|
|
314
|
+
* permanently hide Approve/Deny with no way to retry.
|
|
315
|
+
*/
|
|
316
|
+
askId?: string;
|
|
308
317
|
} | {
|
|
309
318
|
type: "agent_call";
|
|
310
319
|
agent: string;
|
|
@@ -43,6 +43,7 @@ export declare const appConfigSchema: z.ZodObject<{
|
|
|
43
43
|
isWorkspace: z.ZodOptional<z.ZodBoolean>;
|
|
44
44
|
appsJson: z.ZodOptional<z.ZodString>;
|
|
45
45
|
gatewayUrl: z.ZodOptional<z.ZodString>;
|
|
46
|
+
orgDirectoryUrl: z.ZodOptional<z.ZodString>;
|
|
46
47
|
oauthOrigin: z.ZodOptional<z.ZodString>;
|
|
47
48
|
}, z.core.$strip>>;
|
|
48
49
|
}, z.core.$strip>;
|
|
@@ -19,5 +19,6 @@ export declare const workspaceConfig: z.ZodObject<{
|
|
|
19
19
|
isWorkspace: z.ZodOptional<z.ZodBoolean>;
|
|
20
20
|
appsJson: z.ZodOptional<z.ZodString>;
|
|
21
21
|
gatewayUrl: z.ZodOptional<z.ZodString>;
|
|
22
|
+
orgDirectoryUrl: z.ZodOptional<z.ZodString>;
|
|
22
23
|
oauthOrigin: z.ZodOptional<z.ZodString>;
|
|
23
24
|
}, z.core.$strip>;
|
|
@@ -42,6 +42,14 @@ export const workspaceConfig = z.object({
|
|
|
42
42
|
env: ["WORKSPACE_GATEWAY_URL", "VITE_WORKSPACE_GATEWAY_URL"],
|
|
43
43
|
doc: "URL of the workspace gateway fronting this app.",
|
|
44
44
|
}),
|
|
45
|
+
orgDirectoryUrl: z
|
|
46
|
+
.string()
|
|
47
|
+
.min(1)
|
|
48
|
+
.optional()
|
|
49
|
+
.meta({
|
|
50
|
+
env: ["AGENT_NATIVE_ORG_DIRECTORY_URL"],
|
|
51
|
+
doc: "URL of the authoritative organization Dispatch directory.",
|
|
52
|
+
}),
|
|
45
53
|
oauthOrigin: z
|
|
46
54
|
.string()
|
|
47
55
|
.min(1)
|
|
@@ -403,14 +403,14 @@ export declare const chatThreadShares: import("drizzle-orm/sqlite-core").SQLiteT
|
|
|
403
403
|
tableName: string;
|
|
404
404
|
dataType: "string";
|
|
405
405
|
columnType: "SQLiteText";
|
|
406
|
-
data: "org" | "user";
|
|
406
|
+
data: "group" | "org" | "user";
|
|
407
407
|
driverParam: string;
|
|
408
408
|
notNull: true;
|
|
409
409
|
hasDefault: false;
|
|
410
410
|
isPrimaryKey: false;
|
|
411
411
|
isAutoincrement: false;
|
|
412
412
|
hasRuntimeDefault: false;
|
|
413
|
-
enumValues: ["user", "org"];
|
|
413
|
+
enumValues: ["user", "group", "org"];
|
|
414
414
|
baseColumn: never;
|
|
415
415
|
identity: undefined;
|
|
416
416
|
generated: undefined;
|
|
@@ -1371,8 +1371,16 @@ export function useAutoResumeStatus(tabId, forceStopped) {
|
|
|
1371
1371
|
}, [clearAutoResume, forceStopped]);
|
|
1372
1372
|
return { isAutoResuming, clearAutoResume };
|
|
1373
1373
|
}
|
|
1374
|
-
function approvalResolutionIdentity(approvalKey, toolCallId
|
|
1375
|
-
|
|
1374
|
+
function approvalResolutionIdentity(approvalKey, toolCallId,
|
|
1375
|
+
// Included so a NEW `approval_required` ask for the same toolCallId/
|
|
1376
|
+
// approvalKey (the server re-emitting after a failed resume never
|
|
1377
|
+
// consumed the earlier grant) looks up as unresolved instead of
|
|
1378
|
+
// inheriting the earlier ask's retained "approved" mark forever. Two
|
|
1379
|
+
// asks sharing no askId (older events, non-production-agent approval
|
|
1380
|
+
// sources) still collapse onto the same identity, preserving the
|
|
1381
|
+
// existing remount-survives-as-approved behavior for them.
|
|
1382
|
+
askId) {
|
|
1383
|
+
return `${toolCallId ?? ""}\u0000${approvalKey}\u0000${askId ?? ""}`;
|
|
1376
1384
|
}
|
|
1377
1385
|
const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateText, suggestions, dynamicSuggestions, threadFooterSlot, emptyStateAddon, showHeader = true, onSwitchToCli, className, apiUrl, tabId, browserTabId, threadId, contextScope, contextNamespace, isActiveComposer = true, onMessageCountChange, onSaveThread, onGenerateTitle, composerSlot, onComposerTextChange, composerAreaClassName, composerPlaceholder, missingApiKeySetupLayout = "default", composerLayoutVariant = "default", centerComposerWhenEmpty = false, emptyStateDisplay = "default", composerToolbarSlot, composerExtraActionButton, showModelSelector = true, composerDisabled = false, composerDisabledPlaceholder, isNewThread, isThreadStateLoading, onSlashCommand, execMode, onExecModeChange, approvalActions, planModeDisabled, planModeDisabledReason, selectedModel, defaultModel, selectedEngine, selectedEffort, availableModels, modelListLoading, onModelChange, onEffortChange, availableAgents, selectedAgent, hostedHarness, onAgentChange, imageModelMenu, onForkChat, onConnectProvider, onConnectLocalRuntime, plusMenuMode = "full", providerStatusChecksEnabled = true, loadHistoryRepository, historyReloadKey, externalStreaming = false, agentChatSurface = "app", suppressInlineOpenApp = false, }, ref) {
|
|
1378
1386
|
const t = useT();
|
|
@@ -4131,19 +4139,19 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
4131
4139
|
scope: approvalResolutionScope,
|
|
4132
4140
|
byIdentity: new Map(),
|
|
4133
4141
|
}));
|
|
4134
|
-
const getApprovalResolution = useCallback((approvalKey, toolCallId) => {
|
|
4142
|
+
const getApprovalResolution = useCallback((approvalKey, toolCallId, askId) => {
|
|
4135
4143
|
if (approvalResolutionState.scope !== approvalResolutionScope) {
|
|
4136
4144
|
return null;
|
|
4137
4145
|
}
|
|
4138
|
-
return (approvalResolutionState.byIdentity.get(approvalResolutionIdentity(approvalKey, toolCallId)) ?? null);
|
|
4146
|
+
return (approvalResolutionState.byIdentity.get(approvalResolutionIdentity(approvalKey, toolCallId, askId)) ?? null);
|
|
4139
4147
|
}, [approvalResolutionScope, approvalResolutionState]);
|
|
4140
|
-
const recordApprovalResolution = useCallback((approvalKey, resolution, toolCallId) => {
|
|
4148
|
+
const recordApprovalResolution = useCallback((approvalKey, resolution, toolCallId, askId) => {
|
|
4141
4149
|
setApprovalResolutionState((previous) => {
|
|
4142
4150
|
const byIdentity = previous.scope === approvalResolutionScope
|
|
4143
4151
|
? previous.byIdentity
|
|
4144
4152
|
: new Map();
|
|
4145
4153
|
const next = new Map(byIdentity);
|
|
4146
|
-
next.set(approvalResolutionIdentity(approvalKey, toolCallId), resolution);
|
|
4154
|
+
next.set(approvalResolutionIdentity(approvalKey, toolCallId, askId), resolution);
|
|
4147
4155
|
return { scope: approvalResolutionScope, byIdentity: next };
|
|
4148
4156
|
});
|
|
4149
4157
|
}, [approvalResolutionScope]);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AgentNativeDeploymentEnvironment, AgentNativeConfig } from "../config.js";
|
|
2
|
+
export interface EnvironmentBadgeTargets {
|
|
3
|
+
betaHost: string;
|
|
4
|
+
productionHost: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function isBuilderIoEmployee(email: string | null | undefined): boolean;
|
|
7
|
+
export declare function resolveEnvironmentTargets(hostname: string | undefined): EnvironmentBadgeTargets | null;
|
|
8
|
+
export declare function resolveEnvironmentChannel(config: AgentNativeConfig, hostname: string | undefined): Extract<AgentNativeDeploymentEnvironment, "beta" | "production"> | null;
|
|
9
|
+
export declare function buildEnvironmentUrl(sourceHref: string, targetHost: string): string | null;
|
|
10
|
+
/**
|
|
11
|
+
* Small internal-only lane switcher for first-party hosted Agent-Native apps.
|
|
12
|
+
* It is mounted inside the authenticated AppProviders shell so public pages,
|
|
13
|
+
* embeds, and signed-out visitors never receive environment navigation.
|
|
14
|
+
*/
|
|
15
|
+
export declare function EnvironmentBadge(): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from "@agent-native/toolkit/ui/button";
|
|
3
|
+
import { Popover, PopoverContent, PopoverTrigger, } from "@agent-native/toolkit/ui/popover";
|
|
4
|
+
import { IconExternalLink, IconGitBranch } from "@tabler/icons-react";
|
|
5
|
+
import { useMemo } from "react";
|
|
6
|
+
import { injectedAgentNativeConfig } from "./app-config.js";
|
|
7
|
+
import { useSession } from "./use-session.js";
|
|
8
|
+
export function isBuilderIoEmployee(email) {
|
|
9
|
+
return email?.trim().toLowerCase().endsWith("@builder.io") ?? false;
|
|
10
|
+
}
|
|
11
|
+
export function resolveEnvironmentTargets(hostname) {
|
|
12
|
+
const normalized = hostname?.trim().toLowerCase().replace(/\.$/, "");
|
|
13
|
+
if (!normalized)
|
|
14
|
+
return null;
|
|
15
|
+
const productionHost = normalized.replace(/^beta\./, "");
|
|
16
|
+
const isFirstPartyHost = productionHost.endsWith(".agent-native.com") ||
|
|
17
|
+
productionHost === "agent-workspace.builder.io";
|
|
18
|
+
if (!isFirstPartyHost)
|
|
19
|
+
return null;
|
|
20
|
+
return {
|
|
21
|
+
betaHost: `beta.${productionHost}`,
|
|
22
|
+
productionHost,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function resolveEnvironmentChannel(config, hostname) {
|
|
26
|
+
const configured = config.deployment?.environment;
|
|
27
|
+
if (configured === "beta" || configured === "production") {
|
|
28
|
+
return configured;
|
|
29
|
+
}
|
|
30
|
+
const targets = resolveEnvironmentTargets(hostname);
|
|
31
|
+
if (!targets || !hostname)
|
|
32
|
+
return null;
|
|
33
|
+
return hostname.trim().toLowerCase().startsWith("beta.")
|
|
34
|
+
? "beta"
|
|
35
|
+
: "production";
|
|
36
|
+
}
|
|
37
|
+
export function buildEnvironmentUrl(sourceHref, targetHost) {
|
|
38
|
+
try {
|
|
39
|
+
const target = new URL(sourceHref);
|
|
40
|
+
target.protocol = "https:";
|
|
41
|
+
target.hostname = targetHost;
|
|
42
|
+
target.port = "";
|
|
43
|
+
return target.toString();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// coercion-ok: Invalid navigation input is an explicit absent target.
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function EnvironmentLink({ label, href }) {
|
|
51
|
+
return (_jsx(Button, { asChild: true, className: "w-full justify-start px-2", size: "sm", variant: "ghost", children: _jsxs("a", { href: href, children: [_jsx(IconExternalLink, { "aria-hidden": "true" }), label] }) }));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Small internal-only lane switcher for first-party hosted Agent-Native apps.
|
|
55
|
+
* It is mounted inside the authenticated AppProviders shell so public pages,
|
|
56
|
+
* embeds, and signed-out visitors never receive environment navigation.
|
|
57
|
+
*/
|
|
58
|
+
export function EnvironmentBadge() {
|
|
59
|
+
const { session, status } = useSession();
|
|
60
|
+
const config = useMemo(injectedAgentNativeConfig, []);
|
|
61
|
+
const environment = resolveEnvironmentChannel(config, typeof window === "undefined" ? undefined : window.location.hostname);
|
|
62
|
+
const targets = resolveEnvironmentTargets(typeof window === "undefined" ? undefined : window.location.hostname);
|
|
63
|
+
if (typeof window === "undefined" ||
|
|
64
|
+
window.parent !== window ||
|
|
65
|
+
status !== "authenticated" ||
|
|
66
|
+
!isBuilderIoEmployee(session?.email) ||
|
|
67
|
+
!environment ||
|
|
68
|
+
!targets) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const currentHref = window.location.href;
|
|
72
|
+
const betaHref = buildEnvironmentUrl(currentHref, targets.betaHost);
|
|
73
|
+
const productionHref = buildEnvironmentUrl(currentHref, targets.productionHost);
|
|
74
|
+
if (!betaHref || !productionHref)
|
|
75
|
+
return null;
|
|
76
|
+
const label = environment === "beta" ? "beta" : "prod";
|
|
77
|
+
const title = environment === "beta" ? "Beta environment" : "Production environment";
|
|
78
|
+
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { "aria-label": `Open ${title.toLowerCase()} switcher`, className: "fixed bottom-3 right-3 z-[100] h-7 rounded-full border-border/80 bg-background/95 px-2.5 text-[11px] font-semibold uppercase tracking-[0.12em] shadow-sm backdrop-blur-sm", size: "sm", variant: environment === "beta" ? "secondary" : "outline", children: [_jsx("span", { "aria-hidden": "true", className: `size-1.5 rounded-full ${environment === "beta" ? "bg-primary" : "bg-muted-foreground"}` }), label] }) }), _jsxs(PopoverContent, { align: "end", className: "w-64 p-3", side: "top", children: [_jsxs("div", { className: "mb-2 flex items-center gap-2 text-sm font-medium", children: [_jsx(IconGitBranch, { "aria-hidden": "true", className: "size-4 text-muted-foreground" }), title] }), _jsx("div", { className: "grid gap-1", children: environment === "beta" ? (_jsx(EnvironmentLink, { href: productionHref, label: "Go to production" })) : (_jsx(EnvironmentLink, { href: betaHref, label: "Go to beta" })) })] })] }));
|
|
79
|
+
}
|
|
@@ -291,6 +291,22 @@ const DEFAULT_AGENT_TEAM_POLL_MS = 3000;
|
|
|
291
291
|
const DEFAULT_THREAD_URL_PARAM = "thread";
|
|
292
292
|
const THREAD_URL_CHANGED_EVENT = "agent-chat:url-thread-changed";
|
|
293
293
|
const hasOwn = Object.prototype.hasOwnProperty;
|
|
294
|
+
// A duplicated id in `openTabIds` makes two tab-bar entries share one
|
|
295
|
+
// underlying thread: closing either one filters that id out of the array
|
|
296
|
+
// entirely, so both disappear at once. De-duplicate at every boundary the
|
|
297
|
+
// array crosses (localStorage read and write) so a corrupted persisted list
|
|
298
|
+
// self-heals instead of reproducing the phantom tab on every reload.
|
|
299
|
+
function dedupeIds(ids) {
|
|
300
|
+
const seen = new Set();
|
|
301
|
+
const result = [];
|
|
302
|
+
for (const id of ids) {
|
|
303
|
+
if (seen.has(id))
|
|
304
|
+
continue;
|
|
305
|
+
seen.add(id);
|
|
306
|
+
result.push(id);
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
294
310
|
// The history patch is installed once and shared via a ref count so that
|
|
295
311
|
// multiple synced chats (or a remount) don't restore a stale `pushState`
|
|
296
312
|
// reference and silently drop a wrapper installed by another instance.
|
|
@@ -794,7 +810,7 @@ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderO
|
|
|
794
810
|
// (and rebroadcasting their run state) while another resource is open.
|
|
795
811
|
const scopeKeyPart = scope ? `:scope:${scope.type}:${scope.id}` : "";
|
|
796
812
|
const OPEN_TABS_KEY = `agent-chat-open-tabs${keyPrefix}${scopeKeyPart}`;
|
|
797
|
-
const [openTabIds,
|
|
813
|
+
const [openTabIds, setOpenTabIdsRaw] = useState(() => {
|
|
798
814
|
if (!restoreActiveThread && activeThreadId) {
|
|
799
815
|
for (const id of [activeThreadId])
|
|
800
816
|
mountedTabsRef.current.add(id);
|
|
@@ -805,16 +821,35 @@ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderO
|
|
|
805
821
|
if (saved) {
|
|
806
822
|
const parsed = JSON.parse(saved);
|
|
807
823
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
824
|
+
const deduped = dedupeIds(parsed);
|
|
808
825
|
// Mark restored tabs as mounted
|
|
809
|
-
for (const id of
|
|
826
|
+
for (const id of deduped)
|
|
810
827
|
mountedTabsRef.current.add(id);
|
|
811
|
-
return
|
|
828
|
+
return deduped;
|
|
812
829
|
}
|
|
813
830
|
}
|
|
814
831
|
}
|
|
815
832
|
catch { }
|
|
816
833
|
return [];
|
|
817
834
|
});
|
|
835
|
+
// Every writer routes through here so a duplicate id can never reach state.
|
|
836
|
+
// A `.includes()` guard evaluated outside the updater cannot prevent one: a
|
|
837
|
+
// synchronous burst of writes (the mount-time replay of buffered open
|
|
838
|
+
// requests dispatches its whole backlog in one loop) has every handler read
|
|
839
|
+
// the same pre-render list, so each one appends. Two tab-bar entries then
|
|
840
|
+
// share a single thread id and `closeTab`'s filter removes both at once.
|
|
841
|
+
// Returning `prev` unchanged is load-bearing, not an optimization — callers
|
|
842
|
+
// rely on identity bail-outs to keep effects that depend on `openTabIds`
|
|
843
|
+
// from re-running forever.
|
|
844
|
+
const setOpenTabIds = useCallback((value) => {
|
|
845
|
+
setOpenTabIdsRaw((prev) => {
|
|
846
|
+
const next = dedupeIds(typeof value === "function" ? value(prev) : value);
|
|
847
|
+
return next.length === prev.length &&
|
|
848
|
+
next.every((id, i) => id === prev[i])
|
|
849
|
+
? prev
|
|
850
|
+
: next;
|
|
851
|
+
});
|
|
852
|
+
}, []);
|
|
818
853
|
const openTabIdsRef = useRef(openTabIds);
|
|
819
854
|
openTabIdsRef.current = openTabIds;
|
|
820
855
|
const initializedRef = useRef(false);
|
|
@@ -982,6 +1017,7 @@ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderO
|
|
|
982
1017
|
// optimistic id for a brand-new session); the activeThreadId effect above
|
|
983
1018
|
// adds it to openTabIds without spinning up a duplicate thread.
|
|
984
1019
|
const autoCreatingRef = useRef(false);
|
|
1020
|
+
const lastTabReplacementInFlightRef = useRef(false);
|
|
985
1021
|
useEffect(() => {
|
|
986
1022
|
if (isLoading || autoCreatingRef.current)
|
|
987
1023
|
return;
|
|
@@ -1435,25 +1471,45 @@ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderO
|
|
|
1435
1471
|
});
|
|
1436
1472
|
}, []);
|
|
1437
1473
|
const closeTab = useCallback((tabId) => {
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1474
|
+
// Read the current list from the ref rather than a `setOpenTabIds`
|
|
1475
|
+
// updater callback — `createThread()`/`switchThread()` below set
|
|
1476
|
+
// `activeThreadId` synchronously as a side effect, and calling them
|
|
1477
|
+
// from inside an updater left `openTabIds` and `activeThreadId`
|
|
1478
|
+
// inconsistent for a render: the "ensure active thread is in open
|
|
1479
|
+
// tabs" effect would then re-add the just-closed id, so the tab
|
|
1480
|
+
// appeared to reopen itself right after closing.
|
|
1481
|
+
const prev = openTabIdsRef.current;
|
|
1482
|
+
if (prev.length <= 1) {
|
|
1483
|
+
if (lastTabReplacementInFlightRef.current)
|
|
1484
|
+
return;
|
|
1485
|
+
lastTabReplacementInFlightRef.current = true;
|
|
1486
|
+
// Last tab — create a new one and replace the old tab once ready;
|
|
1487
|
+
// the old tab stays visible in the meantime so the bar is never empty.
|
|
1488
|
+
cleanupClosedTab(tabId);
|
|
1489
|
+
void (async () => {
|
|
1490
|
+
try {
|
|
1491
|
+
const newId = await createThread();
|
|
1442
1492
|
if (newId) {
|
|
1443
1493
|
newThreadIds.current.add(newId);
|
|
1444
1494
|
setOpenTabIds([newId]);
|
|
1445
1495
|
writeThreadUrl(null);
|
|
1446
1496
|
}
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
}
|
|
1455
|
-
return
|
|
1456
|
-
}
|
|
1497
|
+
}
|
|
1498
|
+
catch (error) {
|
|
1499
|
+
console.error("[agent-chat] failed to replace the closed final tab", error);
|
|
1500
|
+
}
|
|
1501
|
+
finally {
|
|
1502
|
+
lastTabReplacementInFlightRef.current = false;
|
|
1503
|
+
}
|
|
1504
|
+
})();
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
const next = prev.filter((id) => id !== tabId);
|
|
1508
|
+
if (tabId === activeThreadIdRef.current && next.length > 0) {
|
|
1509
|
+
const idx = prev.indexOf(tabId);
|
|
1510
|
+
switchThread(next[Math.min(idx, next.length - 1)]);
|
|
1511
|
+
}
|
|
1512
|
+
setOpenTabIds(next);
|
|
1457
1513
|
cleanupClosedTab(tabId);
|
|
1458
1514
|
}, [switchThread, createThread, cleanupClosedTab, writeThreadUrl]);
|
|
1459
1515
|
const closeOtherTabs = useCallback((tabId) => {
|