@agent-native/core 0.161.22 → 0.162.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/app/components/dashboard/SqlChart.tsx +18 -2
- package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +1 -1
- 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/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/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/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/design/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +13 -7
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +18 -21
- package/corpus/templates/design/app/pages/design-editor/editor-state.ts +2 -5
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +4 -12
- package/corpus/templates/design/app/pages/design-editor/types.ts +38 -1
- package/corpus/templates/design/changelog/2026-08-18-advanced-design-editor-panels-hidden-by-default.md +6 -0
- package/corpus/templates/design/playwright.config.ts +19 -2
- package/corpus/templates/dispatch/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/factory/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/macros/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +1 -1
- 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/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/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/lib/create-deck-generation.ts +1 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +34 -1
- 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/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/dist/agent/engine/builder-engine.js +15 -8
- package/dist/agent/engine/translate-anthropic.js +27 -2
- package/dist/agent/production-agent.d.ts +17 -2
- package/dist/agent/production-agent.js +48 -17
- 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/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/session-replay.js +58 -15
- package/dist/client/settings/SettingsTabsPage.js +19 -6
- 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/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/config.d.ts +15 -0
- package/dist/config.js +53 -0
- package/dist/extensions/fetch-tool.js +8 -2
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/mcp/embed-app.js +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/provider-api/index.d.ts +10 -0
- package/dist/provider-api/index.js +45 -9
- 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 +10 -3
- package/dist/server/poll.js +7 -3
- package/dist/server/sentry.d.ts +1 -1
- package/dist/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/vite/client.d.ts +5 -0
- package/dist/vite/client.js +64 -2
- 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/default/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
|
@@ -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;
|
|
@@ -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) => {
|
|
@@ -65,7 +65,17 @@ const MAX_HISTORY_ATTACHMENT_CHARS = 60_000;
|
|
|
65
65
|
// the trailing truncation notice still makes a pathological multi-MB paste
|
|
66
66
|
// visibly (not silently) capped.
|
|
67
67
|
const MAX_OUTBOUND_ATTACHMENT_CHARS = 200_000;
|
|
68
|
-
|
|
68
|
+
// An array-length backstop, NOT the reduction policy. Reducing a long thread is
|
|
69
|
+
// Observational Memory's job: it folds older turns into observations/reflections
|
|
70
|
+
// and replaces the raw prefix with a memory block plus a recent-raw window
|
|
71
|
+
// (agent/observational-memory/). But its Observer only engages once a thread
|
|
72
|
+
// passes 30k unobserved tokens, and a count cap of 24 bit long before that — so
|
|
73
|
+
// turns were evicted from the request while compaction still had nothing to say
|
|
74
|
+
// about them, and only reached the model again once thread_data was observed a
|
|
75
|
+
// turn or more later. The two char budgets below are the real bound (they cap
|
|
76
|
+
// what a request can carry regardless of message count); this cap only keeps the
|
|
77
|
+
// array from growing without limit.
|
|
78
|
+
const MAX_HISTORY_MESSAGES = 80;
|
|
69
79
|
// Every provider prompt cache matches a byte-identical PREFIX. A window that
|
|
70
80
|
// ends at the newest message and starts MAX_HISTORY_MESSAGES back moves its
|
|
71
81
|
// START by one message per turn, so from the first turn past the cap onward no
|
package/dist/client/api-path.js
CHANGED
|
@@ -48,7 +48,20 @@ function workspacePathBasePath() {
|
|
|
48
48
|
const segment = window.location.pathname.split("/").find(Boolean);
|
|
49
49
|
if (!segment || segment === "_agent-native" || segment === "api")
|
|
50
50
|
return "";
|
|
51
|
-
|
|
51
|
+
const basePath = normalizeBasePath(segment);
|
|
52
|
+
// Guard against treating an app-local route (e.g. a client-rendered
|
|
53
|
+
// "/settings" page reached via stale client-side navigation) as if it
|
|
54
|
+
// were a sibling app's workspace mount — that built URLs like
|
|
55
|
+
// "/settings/_agent-native/builder/connect", which the workspace gateway
|
|
56
|
+
// 404s (no app is mounted at "/settings") into its app-picker page instead
|
|
57
|
+
// of the real target route. Only trust the segment when it matches a
|
|
58
|
+
// known mount from the deployed app manifest; when the manifest can't be
|
|
59
|
+
// read, fall back to the prior blind-trust behavior (e.g. the same build
|
|
60
|
+
// reused across sibling app ids without a manifest).
|
|
61
|
+
const mounts = workspaceAppMountPaths();
|
|
62
|
+
if (mounts && !mounts.has(basePath))
|
|
63
|
+
return "";
|
|
64
|
+
return basePath;
|
|
52
65
|
}
|
|
53
66
|
function externalEmbedTargetBasePath() {
|
|
54
67
|
if (typeof window === "undefined")
|
|
@@ -57,6 +57,7 @@ import { useInRouterContext } from "react-router";
|
|
|
57
57
|
import { isHumanReadableDocumentTitle, normalizeDocumentTitle, } from "../shared/document-title.js";
|
|
58
58
|
import { ClientOnly } from "./ClientOnly.js";
|
|
59
59
|
import { DefaultSpinner } from "./DefaultSpinner.js";
|
|
60
|
+
import { EnvironmentBadge } from "./EnvironmentBadge.js";
|
|
60
61
|
import { AgentNativeI18nProvider, } from "./i18n.js";
|
|
61
62
|
import { FirstRunOnboardingStartupGate } from "./onboarding/first-run-startup-gate.js";
|
|
62
63
|
import { RequireSession } from "./require-session.js";
|
|
@@ -151,5 +152,5 @@ export function AppProviders({ queryClient, isPublicPath = false, clientOnlyFall
|
|
|
151
152
|
if (isPublicPath) {
|
|
152
153
|
return (_jsx(ProvidersInner, { queryClient: queryClient, defaultTheme: defaultTheme, themeAttribute: themeAttribute, tooltipDelayDuration: tooltipDelayDuration, toaster: toaster, disableThemeTransitions: disableThemeTransitions, i18n: i18n, documentTitleFallback: documentTitleFallback, children: children }));
|
|
153
154
|
}
|
|
154
|
-
return (_jsx(ClientOnly, { fallback: fallback, children: _jsx(ProvidersInner, { queryClient: queryClient, defaultTheme: defaultTheme, themeAttribute: themeAttribute, tooltipDelayDuration: tooltipDelayDuration, toaster: toaster, disableThemeTransitions: disableThemeTransitions, i18n: i18n, documentTitleFallback: documentTitleFallback, children: _jsx(RequireSession, { bypass: sessionBypass, fallback: fallback, children: sessionBypass ? (children) : (
|
|
155
|
+
return (_jsx(ClientOnly, { fallback: fallback, children: _jsx(ProvidersInner, { queryClient: queryClient, defaultTheme: defaultTheme, themeAttribute: themeAttribute, tooltipDelayDuration: tooltipDelayDuration, toaster: toaster, disableThemeTransitions: disableThemeTransitions, i18n: i18n, documentTitleFallback: documentTitleFallback, children: _jsx(RequireSession, { bypass: sessionBypass, fallback: fallback, children: sessionBypass ? (children) : (_jsxs(FirstRunOnboardingStartupGate, { children: [children, _jsx(EnvironmentBadge, {})] })) }) }) }));
|
|
155
156
|
}
|
|
@@ -34,9 +34,17 @@ export type ApprovalContextValue = {
|
|
|
34
34
|
* Keep the visible resolution stable while the chat repository refreshes or
|
|
35
35
|
* remounts the message containing this approval card.
|
|
36
36
|
*/
|
|
37
|
-
onApprovalResolved?: (approvalKey: string, resolution: ApprovalResolution, toolCallId?: string
|
|
37
|
+
onApprovalResolved?: (approvalKey: string, resolution: ApprovalResolution, toolCallId?: string,
|
|
38
|
+
/**
|
|
39
|
+
* Identifies the specific `approval_required` ask being resolved. A
|
|
40
|
+
* remount that replays the SAME ask (e.g. a chat repository refresh)
|
|
41
|
+
* omits nothing new here, so the retained resolution still matches; a
|
|
42
|
+
* fresh ask after a failed resume carries a different `askId` and so
|
|
43
|
+
* looks up as unresolved. See `ApprovalAffordance` below.
|
|
44
|
+
*/
|
|
45
|
+
askId?: string) => void;
|
|
38
46
|
/** Read a resolution retained by the owning chat surface. */
|
|
39
|
-
getApprovalResolution?: (approvalKey: string, toolCallId?: string) => ApprovalResolution | null;
|
|
47
|
+
getApprovalResolution?: (approvalKey: string, toolCallId?: string, askId?: string) => ApprovalResolution | null;
|
|
40
48
|
/**
|
|
41
49
|
* Optional host hook invoked in addition to the local "denied" state, e.g.
|
|
42
50
|
* so a Code session can also resolve its own pending approval as denied.
|
|
@@ -97,6 +105,7 @@ export declare function ToolCallDisplay({ toolName, toolCallId, argsText, args,
|
|
|
97
105
|
approval?: {
|
|
98
106
|
approvalKey: string;
|
|
99
107
|
dismissed?: boolean;
|
|
108
|
+
askId?: string;
|
|
100
109
|
};
|
|
101
110
|
repeatCount?: number;
|
|
102
111
|
/** The latest tool shown while the overall chat turn is still active. */
|
|
@@ -113,6 +122,7 @@ export declare function ToolCallFallback({ toolName, toolCallId, args, argsText,
|
|
|
113
122
|
approval?: {
|
|
114
123
|
approvalKey: string;
|
|
115
124
|
dismissed?: boolean;
|
|
125
|
+
askId?: string;
|
|
116
126
|
};
|
|
117
127
|
repeatCount?: number;
|
|
118
128
|
isLatestRunning?: boolean;
|
|
@@ -16,7 +16,7 @@ import { cn } from "../utils.js";
|
|
|
16
16
|
import { ActionChatUiSurface } from "./action-chat-ui-surface.js";
|
|
17
17
|
import { SmoothMarkdownText, HighlightedCodeBlock, } from "./markdown-renderer.js";
|
|
18
18
|
import { resolveToolRenderer } from "./tool-render-registry.js";
|
|
19
|
-
import { isBuiltinDataWidgetActionRenderer, resolveBuiltinActionChatRenderer, resolveBuiltinFallbackToolRenderer, } from "./widgets/builtin-tool-renderers.js";
|
|
19
|
+
import { isBuiltinDataWidgetActionRenderer, isBuiltinWorkspaceFileResult, resolveBuiltinActionChatRenderer, resolveBuiltinFallbackToolRenderer, } from "./widgets/builtin-tool-renderers.js";
|
|
20
20
|
// Exported so AssistantChatInner can provide a context value.
|
|
21
21
|
export const ChatRunningContext = React.createContext(false);
|
|
22
22
|
export const ChatRunningRunIdContext = React.createContext(null);
|
|
@@ -249,7 +249,7 @@ function ApprovalAffordance({ toolName, toolCallId, approval, }) {
|
|
|
249
249
|
const t = useT();
|
|
250
250
|
const ctx = React.useContext(ApprovalContext);
|
|
251
251
|
const [localResolution, setLocalResolution] = useState(null);
|
|
252
|
-
const retainedResolution = ctx?.getApprovalResolution?.(approval.approvalKey, toolCallId) ?? null;
|
|
252
|
+
const retainedResolution = ctx?.getApprovalResolution?.(approval.approvalKey, toolCallId, approval.askId) ?? null;
|
|
253
253
|
const resolution = retainedResolution ??
|
|
254
254
|
localResolution ??
|
|
255
255
|
(approval.dismissed === true ? "denied" : null);
|
|
@@ -266,15 +266,15 @@ function ApprovalAffordance({ toolName, toolCallId, approval, }) {
|
|
|
266
266
|
}
|
|
267
267
|
return (_jsxs("div", { className: "mt-1.5 flex min-w-0 flex-wrap items-center gap-2 rounded-md border border-border bg-muted/40 px-2.5 py-1.5", children: [_jsx(IconShieldCheck, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }), _jsx("span", { className: "min-w-0 flex-1 text-xs text-muted-foreground", children: t("agentChat.approval.question", { tool: toolName }) }), ctx && (_jsxs("button", { type: "button", onClick: () => {
|
|
268
268
|
setLocalResolution("approved");
|
|
269
|
-
ctx.onApprovalResolved?.(approval.approvalKey, "approved", toolCallId);
|
|
269
|
+
ctx.onApprovalResolved?.(approval.approvalKey, "approved", toolCallId, approval.askId);
|
|
270
270
|
ctx.onApprove(approval.approvalKey);
|
|
271
271
|
}, className: cn("inline-flex shrink-0 items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium transition-colors", "bg-foreground text-background hover:bg-foreground/90"), children: [_jsx(IconCheck, { className: "h-3.5 w-3.5" }), t("agentChat.approval.approve")] })), ctx?.onAlwaysAllow && (_jsxs("button", { type: "button", onClick: () => {
|
|
272
272
|
setLocalResolution("approved");
|
|
273
|
-
ctx.onApprovalResolved?.(approval.approvalKey, "approved", toolCallId);
|
|
273
|
+
ctx.onApprovalResolved?.(approval.approvalKey, "approved", toolCallId, approval.askId);
|
|
274
274
|
ctx.onAlwaysAllow?.(approval.approvalKey);
|
|
275
275
|
}, title: t("agentChat.approval.alwaysAllowHint"), className: cn("inline-flex shrink-0 items-center gap-1 rounded-md border border-border px-2.5 py-1 text-xs font-medium transition-colors", "text-foreground hover:bg-muted"), children: [_jsx(IconShieldCheck, { className: "h-3.5 w-3.5" }), t("agentChat.approval.alwaysAllow")] })), _jsxs("button", { type: "button", onClick: () => {
|
|
276
276
|
setLocalResolution("denied");
|
|
277
|
-
ctx?.onApprovalResolved?.(approval.approvalKey, "denied", toolCallId);
|
|
277
|
+
ctx?.onApprovalResolved?.(approval.approvalKey, "denied", toolCallId, approval.askId);
|
|
278
278
|
ctx?.onDeny?.(approval.approvalKey);
|
|
279
279
|
}, className: cn("inline-flex shrink-0 items-center gap-1 rounded-md border border-border px-2.5 py-1 text-xs font-medium transition-colors", "text-foreground hover:bg-muted"), children: [_jsx(IconX, { className: "h-3.5 w-3.5" }), t("agentChat.approval.deny")] })] }));
|
|
280
280
|
}
|
|
@@ -403,7 +403,8 @@ function ToolCallDisplayGeneric({ toolName, toolCallId, argsText, args, result,
|
|
|
403
403
|
(skipRegistryRenderer ? null : resolveToolRenderer(nativeToolContext)) ??
|
|
404
404
|
resolveBuiltinFallbackToolRenderer(nativeToolContext));
|
|
405
405
|
if (NativeToolRenderer) {
|
|
406
|
-
return (_jsx(ActionChatUiSurface, { context: nativeToolContext, isBuiltinDataWidget: isBuiltinDataWidgetActionRenderer(nativeToolContext)
|
|
406
|
+
return (_jsx(ActionChatUiSurface, { context: nativeToolContext, isBuiltinDataWidget: isBuiltinDataWidgetActionRenderer(nativeToolContext) ||
|
|
407
|
+
isBuiltinWorkspaceFileResult(nativeToolContext), children: _jsx(NativeToolRenderer, { context: nativeToolContext }) }));
|
|
407
408
|
}
|
|
408
409
|
const inputPayload = hasArgs
|
|
409
410
|
? toolInputPayload(toolName, args, {
|
|
@@ -432,7 +433,16 @@ function ToolCallDisplayGeneric({ toolName, toolCallId, argsText, args, result,
|
|
|
432
433
|
}
|
|
433
434
|
return (_jsxs("div", { className: "group/tool my-0.5 w-full overflow-hidden", children: [mcpApp && !(suppressInlineOpenApp && toolName === "open_app") && (_jsx(McpAppRenderer, { app: mcpApp, className: "mb-1.5" })), _jsxs("button", { type: "button", onClick: () => canExpand && setExpanded(!isExpanded), "aria-expanded": canExpand ? isExpanded : undefined, className: cn("flex w-full items-center gap-1.5 rounded-md py-0.5 text-left text-[13px] text-muted-foreground transition-colors", canExpand && "hover:text-foreground", isRunning && "text-muted-foreground"), children: [_jsx("span", { className: "relative flex size-4 shrink-0 items-center justify-center", children: isRunning ? (_jsx(IconLoader2, { className: "size-3.5 animate-spin" })) : isAgentError ? (_jsx(IconCircleX, { className: "size-3.5 text-destructive" })) : isUnknownOutcome ? (_jsx(IconAlertTriangle, { className: "size-3.5 text-muted-foreground" })) : (_jsxs(_Fragment, { children: [_jsx(ToolIcon, { className: cn("size-3.5 transition-opacity", canExpand && "group-hover/tool:opacity-0") }), canExpand && (_jsx(IconChevronRight, { className: cn("absolute size-3.5 opacity-0 transition-[opacity,transform] group-hover/tool:opacity-100", isExpanded && "rotate-90") }))] })) }), _jsx("span", { className: cn("min-w-0 truncate font-normal", isActiveTail && "agent-running-shimmer"), children: displayName }), repeatCount && repeatCount > 1 && (_jsxs("span", { className: "shrink-0 rounded border border-border/60 px-1.5 py-0.5 text-[10px] leading-none text-muted-foreground", title: t("agentChat.tool.repeated", { count: repeatCount }), children: [repeatCount, "x"] }))] }), _jsx(AnimatedCollapse, { open: isExpanded && !isAgentCall && (hasArgs || result !== undefined), children: _jsxs("div", { className: "mt-1 space-y-2 pl-5", children: [inputPayload && (_jsx(SimpleCodeViewer, { text: inputPayload.text, lang: inputPayload.lang })), resultPayload && (_jsx(ToolOutputPopover, { open: outputOpen, onOpenChange: setOutputOpen, title: outputTitle, payload: resultPayload, children: _jsx("button", { type: "button", "aria-label": t("agentChat.tool.viewOutput", {
|
|
434
435
|
tool: toolName,
|
|
435
|
-
}), className: "inline-flex size-6 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:bg-accent hover:text-foreground", children: _jsx(IconCode, { className: "size-3.5" }) }) }))] }) }), isUnknownOutcome && (_jsx("p", { role: "status", className: "ps-5 text-xs text-muted-foreground", children: t("agentChat.tool.interrupted") })), approval && (_jsx(ApprovalAffordance
|
|
436
|
+
}), className: "inline-flex size-6 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:bg-accent hover:text-foreground", children: _jsx(IconCode, { className: "size-3.5" }) }) }))] }) }), isUnknownOutcome && (_jsx("p", { role: "status", className: "ps-5 text-xs text-muted-foreground", children: t("agentChat.tool.interrupted") })), approval && (_jsx(ApprovalAffordance
|
|
437
|
+
// A changed `askId` means the server re-emitted approval_required
|
|
438
|
+
// for this same call (e.g. a failed resume never consumed the
|
|
439
|
+
// prior grant) rather than the same ask re-rendering. Keying on it
|
|
440
|
+
// forces a fresh mount so a stale local "approved" state from the
|
|
441
|
+
// earlier ask can't linger and hide Approve/Deny with no way to
|
|
442
|
+
// retry. Falls back to approvalKey when askId is absent (older
|
|
443
|
+
// events, non-production-agent approval sources) to keep the
|
|
444
|
+
// existing remount-safe behavior unchanged there.
|
|
445
|
+
, { toolName: toolName, toolCallId: toolCallId, approval: approval }, approval.askId ?? approval.approvalKey))] }));
|
|
436
446
|
}
|
|
437
447
|
function AgentCallCell({ agentName, activity, progress, responseText, isRunning, isError, durationMs, }) {
|
|
438
448
|
const t = useT();
|
|
@@ -29,5 +29,5 @@ export function WorkspaceFileWidget({ result, }) {
|
|
|
29
29
|
const { file } = result;
|
|
30
30
|
const FileIcon = fileIcon(file.contentType);
|
|
31
31
|
const href = resourceDownloadUrl(file.resourceId);
|
|
32
|
-
return (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex size-10 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground", children: _jsx(FileIcon, { className: "size-5", "aria-hidden": "true" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "truncate text-sm font-medium", children: file.name }), _jsxs("div", { className: "truncate text-xs text-muted-foreground", children: [formatFileSize(file.sizeBytes, formatNumber), " \u00B7 ", file.path] })] }), _jsxs("a", { href: href, download: file.name, className: "inline-flex shrink-0 items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground no-underline transition-colors hover:bg-primary/90 hover:no-underline", children: [_jsx(IconDownload, { className: "size-3.5", "aria-hidden": "true" }), t("workspaceFile.download")] })] }));
|
|
32
|
+
return (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-border bg-card p-3 text-card-foreground shadow-sm", children: [_jsx("div", { className: "flex size-10 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground", children: _jsx(FileIcon, { className: "size-5", "aria-hidden": "true" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "truncate text-sm font-medium", children: file.name }), _jsxs("div", { className: "truncate text-xs text-muted-foreground", children: [formatFileSize(file.sizeBytes, formatNumber), " \u00B7 ", file.path] })] }), _jsxs("a", { href: href, download: file.name, className: "inline-flex shrink-0 items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground no-underline transition-colors hover:bg-primary/90 hover:no-underline", children: [_jsx(IconDownload, { className: "size-3.5", "aria-hidden": "true" }), t("workspaceFile.download")] })] }));
|
|
33
33
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { type ToolRendererContext, type ToolRendererComponent } from "../tool-render-registry.js";
|
|
2
2
|
export declare function isBuiltinDataWidgetActionRenderer(context: ToolRendererContext): boolean;
|
|
3
|
+
/** True whenever a result renders as a workspace-file card — with or
|
|
4
|
+
* without a static chatUI on the action that produced it (see the
|
|
5
|
+
* shape-based fallback registered below). The widget frames itself, so
|
|
6
|
+
* callers use this the same way they use `isBuiltinDataWidgetActionRenderer`
|
|
7
|
+
* to skip the chatUI-gated outer border and avoid a double frame. */
|
|
8
|
+
export declare function isBuiltinWorkspaceFileResult(context: ToolRendererContext): boolean;
|
|
3
9
|
export declare function resolveBuiltinActionChatRenderer(context: ToolRendererContext): ToolRendererComponent | null;
|
|
4
10
|
export declare function resolveBuiltinFallbackToolRenderer(context: ToolRendererContext): ToolRendererComponent | null;
|
|
@@ -99,6 +99,14 @@ export function isBuiltinDataWidgetActionRenderer(context) {
|
|
|
99
99
|
renderer === ACTION_CHAT_UI_DATA_INSIGHTS_RENDERER ||
|
|
100
100
|
renderer === ACTION_CHAT_UI_DATA_WIDGET_RENDERER);
|
|
101
101
|
}
|
|
102
|
+
/** True whenever a result renders as a workspace-file card — with or
|
|
103
|
+
* without a static chatUI on the action that produced it (see the
|
|
104
|
+
* shape-based fallback registered below). The widget frames itself, so
|
|
105
|
+
* callers use this the same way they use `isBuiltinDataWidgetActionRenderer`
|
|
106
|
+
* to skip the chatUI-gated outer border and avoid a double frame. */
|
|
107
|
+
export function isBuiltinWorkspaceFileResult(context) {
|
|
108
|
+
return normalizeWorkspaceFileResult(context.resultJson) !== null;
|
|
109
|
+
}
|
|
102
110
|
export function resolveBuiltinActionChatRenderer(context) {
|
|
103
111
|
if (context.chatUI?.renderer === ACTION_CHAT_UI_INLINE_EXTENSION_RENDERER &&
|
|
104
112
|
normalizeInlineExtensionToolResult(context)) {
|
|
@@ -143,3 +151,14 @@ registerReservedFallbackToolRenderer({
|
|
|
143
151
|
match: (context) => normalizeActionDataWidgetResult(context) !== null,
|
|
144
152
|
Component: BuiltinDataWidgetRenderer,
|
|
145
153
|
});
|
|
154
|
+
// Shape-based, not chatUI-based: any tool result — from show-workspace-file,
|
|
155
|
+
// or any other action that spreads `{ file: toWorkspaceFileCard(meta) }` into
|
|
156
|
+
// its own result — renders a download card without a second discretionary
|
|
157
|
+
// call to show-workspace-file. Matching by shape (instead of statically
|
|
158
|
+
// tagging every such action with `chatUI`) also keeps read/list-style calls
|
|
159
|
+
// on the same multi-purpose action collapsible when they don't produce a file.
|
|
160
|
+
registerReservedFallbackToolRenderer({
|
|
161
|
+
id: "core.workspace-file",
|
|
162
|
+
match: (context) => normalizeWorkspaceFileResult(context.resultJson) !== null,
|
|
163
|
+
Component: BuiltinWorkspaceFileRenderer,
|
|
164
|
+
});
|