@agent-native/core 0.191.0 → 0.192.0-nightly-20260926045737
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/CHANGELOG.md +4 -0
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +41 -12
- package/corpus/templates/analytics/app/lib/chat-handoff.ts +62 -0
- package/corpus/templates/analytics/changelog/2026-09-25-chat-sidebar-stays-closed-in-settings.md +6 -0
- package/corpus/templates/analytics/changelog/2026-09-25-show-one-recent-prompt-per-chat-turn.md +6 -0
- package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +74 -11
- package/corpus/templates/clips/app/i18n/en-US.ts +6 -0
- package/corpus/templates/clips/changelog/2026-09-25-one-click-builder-storage.md +6 -0
- package/corpus/templates/design/actions/get-design.schema.ts +19 -0
- package/corpus/templates/design/actions/get-design.ts +16 -19
- package/corpus/templates/mail/actions/apply-ai-filter.ts +8 -0
- package/corpus/templates/mail/actions/create-automation.ts +7 -1
- package/corpus/templates/mail/actions/manage-ai-filter-rule-undo.ts +34 -0
- package/corpus/templates/mail/actions/manage-automations.ts +21 -44
- package/corpus/templates/mail/app/components/email/EmailList.tsx +35 -0
- package/corpus/templates/mail/app/components/onboarding/AiInboxSetup.tsx +116 -34
- package/corpus/templates/mail/app/components/settings/AiFilterSection.tsx +108 -70
- package/corpus/templates/mail/app/components/settings/AiRulePromptField.tsx +8 -1
- package/corpus/templates/mail/app/components/settings/JevConnectionPrompt.tsx +171 -0
- package/corpus/templates/mail/app/hooks/use-automations.ts +24 -0
- package/corpus/templates/mail/app/i18n/en-US.ts +8 -2
- package/corpus/templates/mail/app/pages/InboxPage.tsx +12 -2
- package/corpus/templates/mail/changelog/2026-09-25-mail-now-guides-users-to-connect-jev-before-setting-up-triag.md +6 -0
- package/corpus/templates/mail/server/db/schema.ts +17 -0
- package/corpus/templates/mail/server/lib/ai-filter-rule-undo.ts +151 -0
- package/corpus/templates/mail/server/lib/automations.ts +34 -0
- package/corpus/templates/mail/server/plugins/db.ts +18 -0
- package/corpus/templates/mail/server/plugins/mail-jobs.ts +6 -0
- package/dist/client/AssistantChat.js +20 -3
- package/dist/client/BuilderReferralInviteRow.d.ts +3 -0
- package/dist/client/BuilderReferralInviteRow.js +16 -0
- package/dist/client/agent-chat-adapter.js +6 -5
- package/dist/client/chat/run-recovery.js +6 -5
- package/dist/client/settings/UsageSection.js +2 -1
- package/dist/client/settings/index.d.ts +1 -1
- package/dist/client/settings/index.js +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts +4 -1
- package/dist/client/settings/useBuilderStatus.js +32 -16
- package/dist/client/use-agent-chat-running-threads.d.ts +2 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/framework-tools.d.ts +3 -1
- package/dist/framework-tools.js +2 -0
- package/dist/localization/core-messages/ar-SA.js +4 -0
- package/dist/localization/core-messages/de-DE.js +4 -0
- package/dist/localization/core-messages/en-US.d.ts +4 -0
- package/dist/localization/core-messages/en-US.js +4 -0
- package/dist/localization/core-messages/es-ES.js +4 -0
- package/dist/localization/core-messages/fr-FR.js +4 -0
- package/dist/localization/core-messages/hi-IN.js +4 -0
- package/dist/localization/core-messages/ja-JP.js +4 -0
- package/dist/localization/core-messages/ko-KR.js +4 -0
- package/dist/localization/core-messages/pt-BR.js +4 -0
- package/dist/localization/core-messages/zh-CN.js +4 -0
- package/dist/localization/core-messages/zh-TW.js +4 -0
- package/dist/localization/core-messages.d.ts +4 -0
- package/dist/notifications/routes.d.ts +3 -3
- 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 +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +14 -14
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/action-discovery.js +4 -0
- package/dist/server/agent-chat/plugin-options.d.ts +1 -1
- package/dist/server/agent-chat-plugin.js +6 -4
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts +1 -1
- package/dist/server/fusion-app.d.ts +3 -0
- package/dist/server/fusion-app.js +17 -0
- package/dist/shared/builder-referrals.d.ts +10 -0
- package/dist/shared/builder-referrals.js +29 -0
- package/dist/usage/actions/get-builder-referral-info.d.ts +9 -0
- package/dist/usage/actions/get-builder-referral-info.js +26 -0
- package/dist/usage/metrics-store.js +142 -30
- package/dist/vite/action-types-plugin.js +4 -0
- package/docs/content/actions-agent-tools.mdx +2 -1
- package/package.json +3 -2
|
@@ -65,6 +65,23 @@ export const automationRules = table("automation_rules", {
|
|
|
65
65
|
updatedAt: integer("updated_at").notNull(),
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
export const aiFilterRuleUndo = table(
|
|
69
|
+
"mail_ai_filter_rule_undo",
|
|
70
|
+
{
|
|
71
|
+
id: text("id").primaryKey(),
|
|
72
|
+
ownerEmail: text("owner_email").notNull(),
|
|
73
|
+
rulesJson: text("rules_json").notNull(),
|
|
74
|
+
expiresAt: integer("expires_at").notNull(),
|
|
75
|
+
},
|
|
76
|
+
(t) => [
|
|
77
|
+
index("mail_ai_filter_rule_undo_owner_expiry_idx").on(
|
|
78
|
+
t.ownerEmail,
|
|
79
|
+
t.expiresAt,
|
|
80
|
+
),
|
|
81
|
+
index("mail_ai_filter_rule_undo_expires_idx").on(t.expiresAt),
|
|
82
|
+
],
|
|
83
|
+
);
|
|
84
|
+
|
|
68
85
|
export const emailTracking = table("email_tracking", {
|
|
69
86
|
pixelToken: text("pixel_token").primaryKey(),
|
|
70
87
|
messageId: text("message_id").notNull(),
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { fail } from "@agent-native/core/action";
|
|
2
|
+
import { and, eq, gt, inArray, lt } from "drizzle-orm";
|
|
3
|
+
import { nanoid } from "nanoid";
|
|
4
|
+
|
|
5
|
+
import { db, schema } from "../db/index.js";
|
|
6
|
+
|
|
7
|
+
const undoLifetimeSeconds = 60;
|
|
8
|
+
|
|
9
|
+
export async function purgeExpiredMailAiFilterRuleUndoSnapshots(): Promise<void> {
|
|
10
|
+
const now = Math.floor(Date.now() / 1_000);
|
|
11
|
+
await db
|
|
12
|
+
.delete(schema.aiFilterRuleUndo)
|
|
13
|
+
.where(lt(schema.aiFilterRuleUndo.expiresAt, now))
|
|
14
|
+
.returning({ id: schema.aiFilterRuleUndo.id });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function clearMailAiFilterRules(
|
|
18
|
+
ownerEmail: string,
|
|
19
|
+
ids: string[],
|
|
20
|
+
): Promise<string> {
|
|
21
|
+
const uniqueIds = [...new Set(ids)];
|
|
22
|
+
if (
|
|
23
|
+
uniqueIds.length !== ids.length ||
|
|
24
|
+
uniqueIds.length === 0 ||
|
|
25
|
+
uniqueIds.length > 32
|
|
26
|
+
) {
|
|
27
|
+
fail("AI-filter rule ids must be unique and contain 1 to 32 ids.", {
|
|
28
|
+
errorCode: "invalid_rule_ids",
|
|
29
|
+
statusCode: 400,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const now = Math.floor(Date.now() / 1_000);
|
|
34
|
+
const undoId = nanoid(24);
|
|
35
|
+
return db.transaction(async (tx: any) => {
|
|
36
|
+
await tx
|
|
37
|
+
.delete(schema.aiFilterRuleUndo)
|
|
38
|
+
.where(
|
|
39
|
+
and(
|
|
40
|
+
eq(schema.aiFilterRuleUndo.ownerEmail, ownerEmail),
|
|
41
|
+
lt(schema.aiFilterRuleUndo.expiresAt, now),
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
.returning({ id: schema.aiFilterRuleUndo.id });
|
|
45
|
+
|
|
46
|
+
const rules = await tx
|
|
47
|
+
.select()
|
|
48
|
+
.from(schema.automationRules)
|
|
49
|
+
.where(
|
|
50
|
+
and(
|
|
51
|
+
eq(schema.automationRules.ownerEmail, ownerEmail),
|
|
52
|
+
eq(schema.automationRules.domain, "mail"),
|
|
53
|
+
eq(schema.automationRules.kind, "ai-filter"),
|
|
54
|
+
eq(schema.automationRules.enabled, 1),
|
|
55
|
+
inArray(schema.automationRules.id, uniqueIds),
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
.for("update");
|
|
59
|
+
if (rules.length !== uniqueIds.length) {
|
|
60
|
+
fail("Some AI-filter rules changed before they could be cleared.", {
|
|
61
|
+
errorCode: "rules_changed",
|
|
62
|
+
statusCode: 409,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await tx.insert(schema.aiFilterRuleUndo).values({
|
|
67
|
+
id: undoId,
|
|
68
|
+
ownerEmail,
|
|
69
|
+
rulesJson: JSON.stringify(rules),
|
|
70
|
+
expiresAt: now + undoLifetimeSeconds,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const deleted = await tx
|
|
74
|
+
.delete(schema.automationRules)
|
|
75
|
+
.where(
|
|
76
|
+
and(
|
|
77
|
+
eq(schema.automationRules.ownerEmail, ownerEmail),
|
|
78
|
+
eq(schema.automationRules.domain, "mail"),
|
|
79
|
+
eq(schema.automationRules.kind, "ai-filter"),
|
|
80
|
+
eq(schema.automationRules.enabled, 1),
|
|
81
|
+
inArray(schema.automationRules.id, uniqueIds),
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
.returning({ id: schema.automationRules.id });
|
|
85
|
+
if (deleted.length !== uniqueIds.length) {
|
|
86
|
+
fail("Some AI-filter rules changed before they could be cleared.", {
|
|
87
|
+
errorCode: "rules_changed",
|
|
88
|
+
statusCode: 409,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return undoId;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function restoreMailAiFilterRules(
|
|
96
|
+
ownerEmail: string,
|
|
97
|
+
undoId: string,
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
const now = Math.floor(Date.now() / 1_000);
|
|
100
|
+
await db.transaction(async (tx: any) => {
|
|
101
|
+
const [undo] = await tx
|
|
102
|
+
.select()
|
|
103
|
+
.from(schema.aiFilterRuleUndo)
|
|
104
|
+
.where(
|
|
105
|
+
and(
|
|
106
|
+
eq(schema.aiFilterRuleUndo.id, undoId),
|
|
107
|
+
eq(schema.aiFilterRuleUndo.ownerEmail, ownerEmail),
|
|
108
|
+
gt(schema.aiFilterRuleUndo.expiresAt, now),
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
.for("update");
|
|
112
|
+
if (!undo) {
|
|
113
|
+
fail("This rule undo has expired or was already used.", {
|
|
114
|
+
errorCode: "undo_expired",
|
|
115
|
+
statusCode: 410,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const rules = JSON.parse(undo.rulesJson);
|
|
120
|
+
if (
|
|
121
|
+
!Array.isArray(rules) ||
|
|
122
|
+
rules.some(
|
|
123
|
+
(rule) =>
|
|
124
|
+
rule.ownerEmail !== ownerEmail ||
|
|
125
|
+
rule.domain !== "mail" ||
|
|
126
|
+
rule.kind !== "ai-filter" ||
|
|
127
|
+
rule.enabled !== 1,
|
|
128
|
+
)
|
|
129
|
+
) {
|
|
130
|
+
throw new Error("AI-filter undo snapshot is invalid");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await tx.insert(schema.automationRules).values(rules);
|
|
134
|
+
const consumed = await tx
|
|
135
|
+
.delete(schema.aiFilterRuleUndo)
|
|
136
|
+
.where(
|
|
137
|
+
and(
|
|
138
|
+
eq(schema.aiFilterRuleUndo.id, undoId),
|
|
139
|
+
eq(schema.aiFilterRuleUndo.ownerEmail, ownerEmail),
|
|
140
|
+
gt(schema.aiFilterRuleUndo.expiresAt, now),
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
.returning({ id: schema.aiFilterRuleUndo.id });
|
|
144
|
+
if (consumed.length !== 1) {
|
|
145
|
+
fail("This rule undo has expired or was already used.", {
|
|
146
|
+
errorCode: "undo_expired",
|
|
147
|
+
statusCode: 410,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
@@ -1,9 +1,24 @@
|
|
|
1
|
+
import { fail } from "@agent-native/core/action";
|
|
2
|
+
import {
|
|
3
|
+
getJevContextCredentials,
|
|
4
|
+
isJevEnabled,
|
|
5
|
+
} from "@agent-native/core/server";
|
|
1
6
|
import { and, eq, inArray } from "drizzle-orm";
|
|
2
7
|
import { nanoid } from "nanoid";
|
|
3
8
|
|
|
4
9
|
import type { AutomationAction, AutomationRule } from "../../shared/types.js";
|
|
5
10
|
import { db, schema } from "../db/index.js";
|
|
6
11
|
|
|
12
|
+
export async function assertMailJevEnabled(ownerEmail: string): Promise<void> {
|
|
13
|
+
const credentials = await getJevContextCredentials(ownerEmail);
|
|
14
|
+
if (!(await isJevEnabled(credentials))) {
|
|
15
|
+
fail("Jev is not enabled for this account.", {
|
|
16
|
+
errorCode: "jev_not_enabled",
|
|
17
|
+
statusCode: 403,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
7
22
|
export function toApiRule(row: any): AutomationRule {
|
|
8
23
|
const kind = row.kind ?? "automation";
|
|
9
24
|
if (kind !== "automation" && kind !== "ai-filter") {
|
|
@@ -85,6 +100,24 @@ export async function updateAutomationRule(
|
|
|
85
100
|
kind?: "automation" | "ai-filter";
|
|
86
101
|
},
|
|
87
102
|
): Promise<AutomationRule> {
|
|
103
|
+
const [existing] = await db
|
|
104
|
+
.select()
|
|
105
|
+
.from(schema.automationRules)
|
|
106
|
+
.where(ownedRule(ownerEmail, id));
|
|
107
|
+
if (!existing) throw new Error("Rule not found");
|
|
108
|
+
|
|
109
|
+
const existingIsMailAiFilter =
|
|
110
|
+
existing.domain === "mail" &&
|
|
111
|
+
(existing.kind ?? "automation") === "ai-filter";
|
|
112
|
+
const nextDomain = patch.domain ?? existing.domain;
|
|
113
|
+
const nextKind = patch.kind ?? existing.kind ?? "automation";
|
|
114
|
+
const disableOnly =
|
|
115
|
+
Object.keys(patch).length === 1 && patch.enabled === false;
|
|
116
|
+
const nextIsMailAiFilter = nextDomain === "mail" && nextKind === "ai-filter";
|
|
117
|
+
if ((existingIsMailAiFilter || nextIsMailAiFilter) && !disableOnly) {
|
|
118
|
+
await assertMailJevEnabled(ownerEmail);
|
|
119
|
+
}
|
|
120
|
+
|
|
88
121
|
const updates: Record<string, any> = {
|
|
89
122
|
updatedAt: Math.floor(Date.now() / 1_000),
|
|
90
123
|
};
|
|
@@ -134,6 +167,7 @@ export async function consolidateAutomationRules(
|
|
|
134
167
|
actions: AutomationAction[];
|
|
135
168
|
},
|
|
136
169
|
): Promise<boolean> {
|
|
170
|
+
await assertMailJevEnabled(ownerEmail);
|
|
137
171
|
const ids = [input.id, ...input.duplicateIds];
|
|
138
172
|
if (new Set(ids).size !== ids.length) return false;
|
|
139
173
|
|
|
@@ -300,6 +300,24 @@ CREATE INDEX IF NOT EXISTS idx_mail_inbox_push_invalidations_owner_account
|
|
|
300
300
|
ALTER TABLE mail_sync_accounts
|
|
301
301
|
ADD COLUMN IF NOT EXISTS last_push_generation BIGINT NOT NULL DEFAULT 0;`,
|
|
302
302
|
},
|
|
303
|
+
{
|
|
304
|
+
version: 27,
|
|
305
|
+
name: "mail-ai-filter-rule-undo",
|
|
306
|
+
sql: `CREATE TABLE IF NOT EXISTS mail_ai_filter_rule_undo (
|
|
307
|
+
id TEXT PRIMARY KEY,
|
|
308
|
+
owner_email TEXT NOT NULL,
|
|
309
|
+
rules_json TEXT NOT NULL,
|
|
310
|
+
expires_at BIGINT NOT NULL
|
|
311
|
+
);
|
|
312
|
+
CREATE INDEX IF NOT EXISTS mail_ai_filter_rule_undo_owner_expiry_idx
|
|
313
|
+
ON mail_ai_filter_rule_undo(owner_email, expires_at);`,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
version: 28,
|
|
317
|
+
name: "mail-ai-filter-rule-undo-expiry-index",
|
|
318
|
+
sql: `CREATE INDEX IF NOT EXISTS mail_ai_filter_rule_undo_expires_idx
|
|
319
|
+
ON mail_ai_filter_rule_undo(expires_at);`,
|
|
320
|
+
},
|
|
303
321
|
],
|
|
304
322
|
{ table: "mail_migrations" },
|
|
305
323
|
);
|
|
@@ -3,6 +3,7 @@ import { listOAuthAccounts } from "@agent-native/core/oauth-tokens";
|
|
|
3
3
|
import { startIntervalJob } from "@agent-native/core/server/interval-job";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
|
+
import { purgeExpiredMailAiFilterRuleUndoSnapshots } from "../lib/ai-filter-rule-undo.js";
|
|
6
7
|
import { processAutomations } from "../lib/automation-engine.js";
|
|
7
8
|
import { getClientForAccount, startWatch } from "../lib/google-auth.js";
|
|
8
9
|
import {
|
|
@@ -136,6 +137,11 @@ export default () => {
|
|
|
136
137
|
// overlap the next one and send duplicate mail.
|
|
137
138
|
startIntervalJob(
|
|
138
139
|
async () => {
|
|
140
|
+
try {
|
|
141
|
+
await purgeExpiredMailAiFilterRuleUndoSnapshots();
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error("[mail-jobs] AI-filter undo cleanup failed:", err);
|
|
144
|
+
}
|
|
139
145
|
try {
|
|
140
146
|
await processJobs();
|
|
141
147
|
} catch (err) {
|
|
@@ -2582,6 +2582,10 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
2582
2582
|
: getActiveRun()?.runId === runId
|
|
2583
2583
|
? (getActiveRun()?.turnId ?? undefined)
|
|
2584
2584
|
: undefined;
|
|
2585
|
+
const reconnectEventIdentity = {
|
|
2586
|
+
runId,
|
|
2587
|
+
...(logicalTurnId ? { turnId: logicalTurnId } : {}),
|
|
2588
|
+
};
|
|
2585
2589
|
if (reconnectRunIdRef.current === runId)
|
|
2586
2590
|
return true;
|
|
2587
2591
|
// SINGLE-READER OWNERSHIP: never start a second reader while the
|
|
@@ -2664,7 +2668,11 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
2664
2668
|
setReconnectFrozen(false);
|
|
2665
2669
|
setReconnectContent([]);
|
|
2666
2670
|
window.dispatchEvent(new CustomEvent("agentNative.chatRunning", {
|
|
2667
|
-
detail: {
|
|
2671
|
+
detail: {
|
|
2672
|
+
isRunning: true,
|
|
2673
|
+
tabId: tabId || threadId,
|
|
2674
|
+
...reconnectEventIdentity,
|
|
2675
|
+
},
|
|
2668
2676
|
}));
|
|
2669
2677
|
const abortCtrl = new AbortController();
|
|
2670
2678
|
reconnectAbortRef.current = abortCtrl;
|
|
@@ -2979,7 +2987,11 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
2979
2987
|
: RECONNECT_EMPTY_RETRY_MESSAGE,
|
|
2980
2988
|
});
|
|
2981
2989
|
window.dispatchEvent(new CustomEvent("agentNative.chatRunning", {
|
|
2982
|
-
detail: {
|
|
2990
|
+
detail: {
|
|
2991
|
+
isRunning: false,
|
|
2992
|
+
tabId: tabId || threadId,
|
|
2993
|
+
...reconnectEventIdentity,
|
|
2994
|
+
},
|
|
2983
2995
|
}));
|
|
2984
2996
|
return;
|
|
2985
2997
|
}
|
|
@@ -3015,6 +3027,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
3015
3027
|
isRunning: false,
|
|
3016
3028
|
tabId: tabId || threadId,
|
|
3017
3029
|
reason: "failed",
|
|
3030
|
+
...reconnectEventIdentity,
|
|
3018
3031
|
},
|
|
3019
3032
|
}));
|
|
3020
3033
|
return;
|
|
@@ -3055,7 +3068,11 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
3055
3068
|
reconnectAutoRecoveryCountRef.current = 0;
|
|
3056
3069
|
}
|
|
3057
3070
|
window.dispatchEvent(new CustomEvent("agentNative.chatRunning", {
|
|
3058
|
-
detail: {
|
|
3071
|
+
detail: {
|
|
3072
|
+
isRunning: false,
|
|
3073
|
+
tabId: tabId || threadId,
|
|
3074
|
+
...reconnectEventIdentity,
|
|
3075
|
+
},
|
|
3059
3076
|
}));
|
|
3060
3077
|
}
|
|
3061
3078
|
// Skip the final refresh when the adapter runtime is live — importing
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ShareCopyRow } from "@agent-native/toolkit/sharing";
|
|
3
|
+
import { writeClipboardText } from "./clipboard.js";
|
|
4
|
+
import { useT } from "./i18n.js";
|
|
5
|
+
import { useActionQuery } from "./use-action.js";
|
|
6
|
+
import { cn } from "./utils.js";
|
|
7
|
+
export function BuilderReferralInviteRow({ className, }) {
|
|
8
|
+
const t = useT();
|
|
9
|
+
const referralQuery = useActionQuery("get-builder-referral-info", {}, { staleTime: 5 * 60_000 });
|
|
10
|
+
const referralInfo = referralQuery.data;
|
|
11
|
+
if (!referralInfo?.eligible || !referralInfo.inviteUrl)
|
|
12
|
+
return null;
|
|
13
|
+
return (_jsx(ShareCopyRow, { className: cn("min-w-0", className), value: referralInfo.inviteUrl, label: t("agentChat.usage.inviteFriends"), description: t("agentChat.usage.inviteCredits", {
|
|
14
|
+
amount: referralInfo.creditsPerReferral.toLocaleString(),
|
|
15
|
+
}), copyLabel: t("agentChat.usage.copyInviteLink"), copiedLabel: t("agentChat.usage.inviteLinkCopied"), onCopy: writeClipboardText }));
|
|
16
|
+
}
|
|
@@ -1884,7 +1884,7 @@ export function createAgentChatAdapter(options) {
|
|
|
1884
1884
|
// Signal that generation is starting
|
|
1885
1885
|
if (typeof window !== "undefined") {
|
|
1886
1886
|
window.dispatchEvent(new CustomEvent("agentNative.chatRunning", {
|
|
1887
|
-
detail: { isRunning: true, tabId },
|
|
1887
|
+
detail: { isRunning: true, tabId, turnId },
|
|
1888
1888
|
}));
|
|
1889
1889
|
}
|
|
1890
1890
|
const content = [];
|
|
@@ -1907,9 +1907,10 @@ export function createAgentChatAdapter(options) {
|
|
|
1907
1907
|
pendingTurnBelongsToSurface &&
|
|
1908
1908
|
(pendingTurn.threadId !== threadId || pendingTurn.turnId !== turnId));
|
|
1909
1909
|
};
|
|
1910
|
-
const activeRunMatchesTab = (activeRun) => !activeRun
|
|
1911
|
-
|
|
1912
|
-
|
|
1910
|
+
const activeRunMatchesTab = (activeRun) => !activeRun ||
|
|
1911
|
+
(activeRun.tabId
|
|
1912
|
+
? !activeRunTabId || activeRun.tabId === activeRunTabId
|
|
1913
|
+
: !threadId || activeRun.threadId === threadId);
|
|
1913
1914
|
const ownsActiveRunState = () => {
|
|
1914
1915
|
if (hasPendingSuccessorRequest())
|
|
1915
1916
|
return false;
|
|
@@ -1949,7 +1950,7 @@ export function createAgentChatAdapter(options) {
|
|
|
1949
1950
|
return;
|
|
1950
1951
|
dispatchTerminalChatUiCleanup(tabId);
|
|
1951
1952
|
window.dispatchEvent(new CustomEvent("agentNative.chatRunning", {
|
|
1952
|
-
detail: { isRunning: false, tabId },
|
|
1953
|
+
detail: { isRunning: false, tabId, turnId },
|
|
1953
1954
|
}));
|
|
1954
1955
|
};
|
|
1955
1956
|
const settleTerminalChatRun = () => {
|
|
@@ -11,6 +11,7 @@ import { isCreditsLimitErrorCode } from "../../agent/engine/error-detail.js";
|
|
|
11
11
|
import { buildSettingsRoute } from "../../navigation/index.js";
|
|
12
12
|
import { withBuilderUtmTrackingParams } from "../../shared/builder-link-tracking.js";
|
|
13
13
|
import { agentNativePath } from "../api-path.js";
|
|
14
|
+
import { BuilderReferralInviteRow } from "../BuilderReferralInviteRow.js";
|
|
14
15
|
import { writeClipboardText } from "../clipboard.js";
|
|
15
16
|
import { isProviderAuthenticationError, localizeKnownChatErrorText, } from "../error-format.js";
|
|
16
17
|
import { useFormatters, useT } from "../i18n.js";
|
|
@@ -322,11 +323,11 @@ export function RunErrorRecoveryCard({ info, onContinue, onRetry, onFork, onDism
|
|
|
322
323
|
: handleMissingProviderConnected }), _jsx("div", { className: "flex justify-center px-3 pt-1", children: _jsxs("button", { type: "button", onClick: handleMissingProviderRetry, disabled: retryRequested, className: "inline-flex h-8 items-center gap-1.5 rounded-md bg-foreground px-3 text-xs font-medium text-background hover:opacity-90 disabled:cursor-wait disabled:opacity-60", children: [_jsx(IconRefresh, { size: 13 }), t("agentChat.common.retry")] }) })] }));
|
|
323
324
|
}
|
|
324
325
|
if (isBuilderCreditsLimit) {
|
|
325
|
-
return (
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
326
|
+
return (_jsxs("div", { className: "min-w-0 rounded-lg border border-border bg-card p-3 text-sm", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-3", children: [_jsx("p", { className: "min-w-0 flex-1 font-medium text-foreground", children: t("agentChat.errorMessages.creditsLimitReached", {
|
|
327
|
+
defaultValue: "You've reached your AI credits limit.",
|
|
328
|
+
}) }), _jsx(Button, { asChild: true, size: "sm", children: _jsxs("a", { href: builderSubscriptionUrl, target: "_blank", rel: "noreferrer", children: [t("agentChat.errorMessages.addCreditsInBuilder", {
|
|
329
|
+
defaultValue: "Add credits in Builder",
|
|
330
|
+
}), _jsx(IconArrowUpRight, {})] }) }), _jsx("button", { type: "button", onClick: onDismiss, "aria-label": t("agentChat.common.dismiss"), className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground", children: _jsx(IconX, { size: 14 }) })] }), _jsx(BuilderReferralInviteRow, { className: "mt-3 border-t border-border/70 pt-3" })] }));
|
|
330
331
|
}
|
|
331
332
|
return (_jsxs("div", { className: "min-w-0 rounded-lg border border-amber-500/25 bg-amber-500/[0.06] p-3 text-sm", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-amber-500/10 text-amber-700 dark:text-amber-300", children: _jsx(IconAlertTriangle, { size: 14 }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "font-medium text-foreground", children: runErrorHeadline(info, {
|
|
332
333
|
recoverable: t("agentChat.error.stopped"),
|
|
@@ -9,6 +9,7 @@ import { IconArrowUpRight, IconBell, IconChartLine, IconChevronDown, IconLoader2
|
|
|
9
9
|
import { useState } from "react";
|
|
10
10
|
import { Link } from "react-router";
|
|
11
11
|
import { withBuilderUtmTrackingParams } from "../../shared/builder-link-tracking.js";
|
|
12
|
+
import { BuilderReferralInviteRow } from "../BuilderReferralInviteRow.js";
|
|
12
13
|
import { useT } from "../i18n.js";
|
|
13
14
|
import { useActionMutation, useActionQuery } from "../use-action.js";
|
|
14
15
|
const builderAddCreditsUrl = withBuilderUtmTrackingParams("https://builder.io/account/subscription?signupSource=agent-native", { content: "usage_credit_balance" });
|
|
@@ -192,7 +193,7 @@ function BuilderCreditUsagePanel({ usage }) {
|
|
|
192
193
|
} }) }), _jsx("div", { className: "mt-1 text-right text-xs tabular-nums text-muted-foreground", children: t("agentChat.usage.creditRemaining", {
|
|
193
194
|
defaultValue: "{{amount}} remaining",
|
|
194
195
|
amount: remaining,
|
|
195
|
-
}) })] })] }));
|
|
196
|
+
}) }), _jsx(BuilderReferralInviteRow, { className: "mt-4 border-t border-border/70 pt-4" })] })] }));
|
|
196
197
|
}
|
|
197
198
|
function AlertEditor({ draft, onChange, onSave, onCancel, isPending, }) {
|
|
198
199
|
const t = useT();
|
|
@@ -2,7 +2,7 @@ export { AgentSettingsContent, areExtensionSettingsEnabled, SettingsPanel, useAg
|
|
|
2
2
|
export { getAgentSettingsSearchTabs, type AgentSettingsSearchTab, } from "./agent-settings-search.js";
|
|
3
3
|
export { SettingsTabsPage, type SettingsSearchEntry, type SettingsTabItem, type SettingsTabsPageProps, } from "./SettingsTabsPage.js";
|
|
4
4
|
export { AccountSettingsCard, AccountSettingsForm, type AccountSettingsCardProps, type AccountSettingsFormProps, } from "./AccountSettingsCard.js";
|
|
5
|
-
export { openBuilderConnectPopup, useBuilderConnectFlow, useBuilderStatus, withBuilderConnectTrackingParams, type BuilderConnectFlow, type BuilderConnectFlowOptions, type BuilderConnectStartOptions, type BuilderStatus, type OpenBuilderConnectPopupOptions, } from "./useBuilderStatus.js";
|
|
5
|
+
export { hasBuilderOAuthCredential, openBuilderConnectPopup, useBuilderConnectFlow, useBuilderStatus, withBuilderConnectTrackingParams, type BuilderConnectFlow, type BuilderConnectFlowOptions, type BuilderConnectStartOptions, type BuilderStatus, type OpenBuilderConnectPopupOptions, } from "./useBuilderStatus.js";
|
|
6
6
|
export { DeferredBuilderConnectPopover as BuilderConnectPopover } from "./deferred-builder-connect-popover.js";
|
|
7
7
|
export type { BuilderConnectPopoverProps } from "./BuilderConnectPopover.js";
|
|
8
8
|
export { NewKeyMenu, normalizeKeyName, type NewKeyMenuProps, type NewKeyOption, } from "./NewKeyMenu.js";
|
|
@@ -2,7 +2,7 @@ export { AgentSettingsContent, areExtensionSettingsEnabled, SettingsPanel, useAg
|
|
|
2
2
|
export { getAgentSettingsSearchTabs, } from "./agent-settings-search.js";
|
|
3
3
|
export { SettingsTabsPage, } from "./SettingsTabsPage.js";
|
|
4
4
|
export { AccountSettingsCard, AccountSettingsForm, } from "./AccountSettingsCard.js";
|
|
5
|
-
export { openBuilderConnectPopup, useBuilderConnectFlow, useBuilderStatus, withBuilderConnectTrackingParams, } from "./useBuilderStatus.js";
|
|
5
|
+
export { hasBuilderOAuthCredential, openBuilderConnectPopup, useBuilderConnectFlow, useBuilderStatus, withBuilderConnectTrackingParams, } from "./useBuilderStatus.js";
|
|
6
6
|
export { DeferredBuilderConnectPopover as BuilderConnectPopover } from "./deferred-builder-connect-popover.js";
|
|
7
7
|
export { NewKeyMenu, normalizeKeyName, } from "./NewKeyMenu.js";
|
|
8
8
|
export { SecretsSection } from "./SecretsSection.js";
|
|
@@ -59,6 +59,9 @@ export interface BuilderStatus {
|
|
|
59
59
|
at: number;
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
+
export declare function hasBuilderOAuthCredential(status: Pick<BuilderStatus, "configured" | "envManaged"> & {
|
|
63
|
+
credentialSource?: BuilderStatus["credentialSource"] | null;
|
|
64
|
+
}): boolean;
|
|
62
65
|
/**
|
|
63
66
|
* Fetches Builder connection status from the neutral connection-status route.
|
|
64
67
|
* The legacy /_agent-native/builder/status route remains available for older
|
|
@@ -85,7 +88,7 @@ export interface BuilderConnectFlowOptions {
|
|
|
85
88
|
trackingSource?: string;
|
|
86
89
|
/** Product flow that needed Builder connect, e.g. connect_llm. */
|
|
87
90
|
trackingFlow?: string;
|
|
88
|
-
/** Invoked
|
|
91
|
+
/** Invoked when the current user/org/workspace Builder credential is ready. */
|
|
89
92
|
onConnected?: (state: {
|
|
90
93
|
orgName: string | null;
|
|
91
94
|
}) => void | Promise<void>;
|
|
@@ -7,6 +7,11 @@ import { openMcpAppHostLink } from "../mcp-app-host.js";
|
|
|
7
7
|
import { oauthPopupWaitingUrl } from "../oauth-popup.js";
|
|
8
8
|
import { scheduleAfterPaint } from "../use-after-paint.js";
|
|
9
9
|
import { usePollLoop } from "../use-poll-loop.js";
|
|
10
|
+
export function hasBuilderOAuthCredential(status) {
|
|
11
|
+
return (status.configured &&
|
|
12
|
+
status.credentialSource !== "env" &&
|
|
13
|
+
(!status.envManaged || status.credentialSource != null));
|
|
14
|
+
}
|
|
10
15
|
/**
|
|
11
16
|
* Fetches Builder connection status from the neutral connection-status route.
|
|
12
17
|
* The legacy /_agent-native/builder/status route remains available for older
|
|
@@ -660,10 +665,12 @@ export function useBuilderConnectFlow(opts = {}) {
|
|
|
660
665
|
statusConnectUrlAtRef.current = nextConnectUrl ? Date.now() : null;
|
|
661
666
|
const org = s.orgName ?? null;
|
|
662
667
|
setOrgName(org);
|
|
663
|
-
|
|
668
|
+
const hasOAuthCredential = hasBuilderOAuthCredential(s);
|
|
669
|
+
if (hasOAuthCredential) {
|
|
664
670
|
connectStartedAtRef.current = null;
|
|
671
|
+
setConnecting(false);
|
|
665
672
|
}
|
|
666
|
-
if (
|
|
673
|
+
if (hasOAuthCredential && !notifiedConnectedRef.current) {
|
|
667
674
|
notifiedConnectedRef.current = true;
|
|
668
675
|
notifyAgentEngineConfiguredChanged("builder-status");
|
|
669
676
|
try {
|
|
@@ -673,7 +680,7 @@ export function useBuilderConnectFlow(opts = {}) {
|
|
|
673
680
|
// The caller's callback is a UI convenience; status is already set.
|
|
674
681
|
}
|
|
675
682
|
}
|
|
676
|
-
else if (!
|
|
683
|
+
else if (!hasOAuthCredential) {
|
|
677
684
|
notifiedConnectedRef.current = false;
|
|
678
685
|
}
|
|
679
686
|
// Surface persisted auth-failure messages on idle refreshes, but don't
|
|
@@ -993,31 +1000,39 @@ export function useBuilderConnectFlow(opts = {}) {
|
|
|
993
1000
|
if (started == null)
|
|
994
1001
|
return;
|
|
995
1002
|
const s = await fetchStatus(signal, connectAttemptIdRef.current ?? undefined);
|
|
996
|
-
if (!mountedRef.current)
|
|
1003
|
+
if (!mountedRef.current || connectStartedAtRef.current !== started) {
|
|
997
1004
|
return;
|
|
998
|
-
|
|
1005
|
+
}
|
|
1006
|
+
const orgName = s?.orgName ?? null;
|
|
1007
|
+
if (s) {
|
|
1008
|
+
if (statusUnavailableRef.current) {
|
|
1009
|
+
statusUnavailableRef.current = false;
|
|
1010
|
+
setError(null);
|
|
1011
|
+
}
|
|
1012
|
+
setHasFetchedStatus(true);
|
|
999
1013
|
setStatusResolved(true);
|
|
1000
|
-
|
|
1001
|
-
setConfigured(true);
|
|
1014
|
+
setConfigured(!!s.configured);
|
|
1002
1015
|
setCodeChangeConfigured(isCodeChangeConfigured(s));
|
|
1003
1016
|
setEnvManaged(!!s.envManaged);
|
|
1004
1017
|
setCredentialSource(s.credentialSource ?? null);
|
|
1005
1018
|
setCanDisconnect(!!s.canDisconnect);
|
|
1006
1019
|
setAgentNativeProvisioningEnabled(!!s.agentNativeProvisioningEnabled);
|
|
1007
1020
|
setAgentNativeProvisioningToken(s.agentNativeProvisioningToken ?? null);
|
|
1008
|
-
setAccountExists(
|
|
1021
|
+
setAccountExists(s.connectError?.code === "account_exists");
|
|
1009
1022
|
setBuilderEnabled(!!s.builderEnabled);
|
|
1010
1023
|
const nextConnectUrl = s.connectUrl ?? null;
|
|
1011
1024
|
setStatusConnectUrl(nextConnectUrl);
|
|
1012
1025
|
statusConnectUrlAtRef.current = nextConnectUrl ? Date.now() : null;
|
|
1013
|
-
|
|
1014
|
-
|
|
1026
|
+
setOrgName(orgName);
|
|
1027
|
+
}
|
|
1028
|
+
if (s && hasBuilderOAuthCredential(s)) {
|
|
1029
|
+
setAccountExists(false);
|
|
1015
1030
|
setConnecting(false);
|
|
1016
1031
|
connectStartedAtRef.current = null;
|
|
1017
1032
|
notifiedConnectedRef.current = true;
|
|
1018
1033
|
notifyAgentEngineConfiguredChanged("builder-connect");
|
|
1019
1034
|
try {
|
|
1020
|
-
await onConnectedRef.current?.({ orgName
|
|
1035
|
+
await onConnectedRef.current?.({ orgName });
|
|
1021
1036
|
}
|
|
1022
1037
|
catch {
|
|
1023
1038
|
// coercion-ok: the connection itself succeeded and the UI state is
|
|
@@ -1162,7 +1177,7 @@ export function useBuilderConnectFlow(opts = {}) {
|
|
|
1162
1177
|
connectStartedAtRef.current !== started) {
|
|
1163
1178
|
return;
|
|
1164
1179
|
}
|
|
1165
|
-
if (s
|
|
1180
|
+
if ((s && hasBuilderOAuthCredential(s)) ||
|
|
1166
1181
|
isCurrentConnectError(s?.connectError, started)) {
|
|
1167
1182
|
break;
|
|
1168
1183
|
}
|
|
@@ -1186,7 +1201,8 @@ export function useBuilderConnectFlow(opts = {}) {
|
|
|
1186
1201
|
}
|
|
1187
1202
|
if (callbackSuccessInFlightAtRef.current === started) {
|
|
1188
1203
|
callbackSuccessInFlightAtRef.current = null;
|
|
1189
|
-
if (popupClosedAtRef.current !== null &&
|
|
1204
|
+
if (popupClosedAtRef.current !== null &&
|
|
1205
|
+
(!s || !hasBuilderOAuthCredential(s))) {
|
|
1190
1206
|
popupClosedAtRef.current = Date.now();
|
|
1191
1207
|
}
|
|
1192
1208
|
}
|
|
@@ -1196,15 +1212,15 @@ export function useBuilderConnectFlow(opts = {}) {
|
|
|
1196
1212
|
}
|
|
1197
1213
|
if (!s)
|
|
1198
1214
|
return;
|
|
1199
|
-
if (!s
|
|
1215
|
+
if (!hasBuilderOAuthCredential(s)) {
|
|
1200
1216
|
const connectError = isCurrentConnectError(s?.connectError, started)
|
|
1201
1217
|
? s?.connectError
|
|
1202
1218
|
: null;
|
|
1203
1219
|
setHasFetchedStatus(true);
|
|
1204
1220
|
if (s) {
|
|
1205
1221
|
setStatusResolved(true);
|
|
1206
|
-
setConfigured(
|
|
1207
|
-
setCodeChangeConfigured(
|
|
1222
|
+
setConfigured(!!s.configured);
|
|
1223
|
+
setCodeChangeConfigured(isCodeChangeConfigured(s));
|
|
1208
1224
|
setEnvManaged(!!s.envManaged);
|
|
1209
1225
|
setCredentialSource(s.credentialSource ?? null);
|
|
1210
1226
|
setCanDisconnect(!!s.canDisconnect);
|
|
@@ -8,6 +8,8 @@ export interface AgentChatRunningEventDetail {
|
|
|
8
8
|
threadId?: string;
|
|
9
9
|
/** Legacy alias used by AssistantChat and embedded chat frames. */
|
|
10
10
|
tabId?: string;
|
|
11
|
+
/** Logical chat turn that may continue across multiple server runs. */
|
|
12
|
+
turnId?: string;
|
|
11
13
|
runId?: string;
|
|
12
14
|
reason?: string;
|
|
13
15
|
}
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
|
|
14
14
|
*/
|
|
15
15
|
export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
16
|
-
error: string;
|
|
17
16
|
ok?: undefined;
|
|
17
|
+
error: string;
|
|
18
18
|
} | {
|
|
19
19
|
error?: undefined;
|
|
20
20
|
ok: boolean;
|
|
@@ -14,7 +14,7 @@ import { type DatabaseToolsOption } from "./scripts/db/tool-mode.js";
|
|
|
14
14
|
* when it names them in `initialToolNames`. They stay in the searchable registry
|
|
15
15
|
* either way.
|
|
16
16
|
*/
|
|
17
|
-
export declare const FRAMEWORK_TOOL_GROUPS: readonly ["sharing", "review", "history", "featureFlags", "launchDarkly", "labs", "localization", "audit", "contextXray", "userProfile", "automation", "docs", "resources", "web", "workspaceApps", "chat", "email", "emailCatalog", "workspaceUserGroups", "orgServiceTokens", "orgAdministration"];
|
|
17
|
+
export declare const FRAMEWORK_TOOL_GROUPS: readonly ["sharing", "review", "history", "featureFlags", "launchDarkly", "labs", "localization", "audit", "contextXray", "userProfile", "automation", "docs", "resources", "browserSessions", "web", "workspaceApps", "chat", "email", "emailCatalog", "workspaceUserGroups", "orgServiceTokens", "orgAdministration"];
|
|
18
18
|
export type FrameworkToolGroup = (typeof FRAMEWORK_TOOL_GROUPS)[number];
|
|
19
19
|
/**
|
|
20
20
|
* Per-group switches for the framework's own agent tools. Every group defaults
|
|
@@ -71,6 +71,8 @@ export interface FrameworkToolsOption {
|
|
|
71
71
|
docs?: boolean;
|
|
72
72
|
/** The `resources` tool — workspace notes, memory, and context files. */
|
|
73
73
|
resources?: boolean;
|
|
74
|
+
/** Raw browser-session controls such as `run-browser-session-action`. */
|
|
75
|
+
browserSessions?: boolean;
|
|
74
76
|
/** `web-request` and `web-search`. */
|
|
75
77
|
web?: boolean;
|
|
76
78
|
/** `describe-workspace-apps` and `call-agent` for cross-app delegation. */
|
package/dist/framework-tools.js
CHANGED
|
@@ -28,6 +28,7 @@ export const FRAMEWORK_TOOL_GROUPS = [
|
|
|
28
28
|
"automation",
|
|
29
29
|
"docs",
|
|
30
30
|
"resources",
|
|
31
|
+
"browserSessions",
|
|
31
32
|
"web",
|
|
32
33
|
"workspaceApps",
|
|
33
34
|
"chat",
|
|
@@ -161,6 +162,7 @@ export const CORE_ACTION_GROUPS = {
|
|
|
161
162
|
"manage-usage-alert": "automation",
|
|
162
163
|
"get-usage-metrics": "automation",
|
|
163
164
|
"get-builder-credit-usage": "automation",
|
|
165
|
+
"get-builder-referral-info": "automation",
|
|
164
166
|
"context-manifest-get": "contextXray",
|
|
165
167
|
"context-preview-get": "contextXray",
|
|
166
168
|
"context-pin": "contextXray",
|