@agent-native/core 0.106.1 → 0.106.2
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/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/artifact-response.ts +84 -0
- package/corpus/core/src/agent/thread-data-builder.ts +1 -1
- package/corpus/core/src/integrations/webhook-handler.ts +41 -5
- package/corpus/core/src/server/agent-chat-plugin.ts +7 -2
- package/corpus/templates/clips/app/components/capture-install-options.tsx +60 -5
- package/corpus/templates/clips/app/components/library/library-layout.tsx +9 -1
- package/corpus/templates/clips/app/hooks/use-desktop-promo.ts +4 -4
- package/corpus/templates/clips/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/clips/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +1 -0
- package/corpus/templates/clips/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/clips/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/clips/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/clips/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/clips/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/clips/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/clips/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/clips/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/clips/app/lib/capture-install-options.ts +89 -4
- package/corpus/templates/clips/app/routes/_app.dictate.tsx +10 -1
- package/corpus/templates/clips/app/routes/record.tsx +1 -0
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +7 -0
- package/corpus/templates/clips/changelog/2026-07-16-the-desktop-app-button-now-shows-open-desktop-app-and-launch.md +6 -0
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +5 -0
- package/corpus/templates/clips/desktop/src-tauri/capabilities/default.json +1 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +41 -6
- package/corpus/templates/clips/desktop/src-tauri/tauri.conf.json +5 -0
- package/corpus/templates/content/.agents/skills/content/SKILL.md +31 -0
- package/corpus/templates/content/changelog/2026-07-16-slack-corrections-now-preserve-newer-content-values-unless-y.md +6 -0
- package/dist/a2a/artifact-response.d.ts +9 -0
- package/dist/a2a/artifact-response.d.ts.map +1 -1
- package/dist/a2a/artifact-response.js +68 -0
- package/dist/a2a/artifact-response.js.map +1 -1
- package/dist/agent/thread-data-builder.js +1 -1
- package/dist/agent/thread-data-builder.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/integrations/webhook-handler.d.ts.map +1 -1
- package/dist/integrations/webhook-handler.js +24 -5
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +3 -1
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.106.
|
|
3
|
+
"version": "0.106.2",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -3,6 +3,8 @@ import { createHmac, timingSafeEqual } from "node:crypto";
|
|
|
3
3
|
export interface A2AToolResultSummary {
|
|
4
4
|
tool: string;
|
|
5
5
|
result: string;
|
|
6
|
+
isError?: boolean;
|
|
7
|
+
completedSideEffect?: boolean;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
export interface A2AArtifactResponseOptions {
|
|
@@ -34,6 +36,7 @@ const ARTIFACT_IDENTITY_WRITE_TOOLS = new Set([
|
|
|
34
36
|
"add-database-item",
|
|
35
37
|
"create-document",
|
|
36
38
|
"update-document",
|
|
39
|
+
"set-document-property",
|
|
37
40
|
"create-deck",
|
|
38
41
|
"duplicate-deck",
|
|
39
42
|
"add-slide",
|
|
@@ -545,6 +548,8 @@ function collectArtifacts(results: A2AToolResultSummary[]): {
|
|
|
545
548
|
const forms = new Map<string, CreatedFormArtifact>();
|
|
546
549
|
|
|
547
550
|
for (const toolResult of results) {
|
|
551
|
+
if (toolResult.isError === true || toolResult.completedSideEffect === false)
|
|
552
|
+
continue;
|
|
548
553
|
if (toolResult.tool === "call-agent") {
|
|
549
554
|
for (const artifact of parseDownstreamArtifactBlock(toolResult.result)) {
|
|
550
555
|
if (artifact.kind === "deck") {
|
|
@@ -632,6 +637,7 @@ function collectArtifacts(results: A2AToolResultSummary[]): {
|
|
|
632
637
|
toolResult.tool === "create-document" ||
|
|
633
638
|
toolResult.tool === "update-document"
|
|
634
639
|
) {
|
|
640
|
+
if (parsed.conflict === true) continue;
|
|
635
641
|
const id = stringValue(parsed.id);
|
|
636
642
|
if (id) {
|
|
637
643
|
documents.set(id, {
|
|
@@ -643,6 +649,17 @@ function collectArtifacts(results: A2AToolResultSummary[]): {
|
|
|
643
649
|
continue;
|
|
644
650
|
}
|
|
645
651
|
|
|
652
|
+
if (toolResult.tool === "set-document-property") {
|
|
653
|
+
const id = stringValue(parsed.documentId);
|
|
654
|
+
if (id) {
|
|
655
|
+
documents.set(id, {
|
|
656
|
+
id,
|
|
657
|
+
url: stringValue(parsed.url) ?? stringValue(parsed.urlPath),
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
|
|
646
663
|
if (
|
|
647
664
|
toolResult.tool === "get-document" ||
|
|
648
665
|
toolResult.tool === "get-content-document"
|
|
@@ -872,6 +889,8 @@ export function extractA2AArtifactIdentities(
|
|
|
872
889
|
};
|
|
873
890
|
|
|
874
891
|
for (const result of results) {
|
|
892
|
+
if (result.isError === true || result.completedSideEffect === false)
|
|
893
|
+
continue;
|
|
875
894
|
if (result.tool === "call-agent") {
|
|
876
895
|
for (const identity of persistedArtifactIdentitiesFromMarker(
|
|
877
896
|
result.result,
|
|
@@ -1540,3 +1559,68 @@ export function buildA2ARecoverableArtifactMessage(
|
|
|
1540
1559
|
...lines,
|
|
1541
1560
|
].join("\n");
|
|
1542
1561
|
}
|
|
1562
|
+
|
|
1563
|
+
function mutationReceiptUrl(
|
|
1564
|
+
identity: A2AArtifactIdentity,
|
|
1565
|
+
baseUrl: string | undefined,
|
|
1566
|
+
): string | undefined {
|
|
1567
|
+
if (
|
|
1568
|
+
identity.sourceAction === "call-agent" &&
|
|
1569
|
+
(!identity.url || identity.url.startsWith("/"))
|
|
1570
|
+
) {
|
|
1571
|
+
return undefined;
|
|
1572
|
+
}
|
|
1573
|
+
if (identity.url) {
|
|
1574
|
+
return identity.url.startsWith("/")
|
|
1575
|
+
? artifactUrl(baseUrl, identity.url)
|
|
1576
|
+
: identity.url;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
const path =
|
|
1580
|
+
identity.resourceType === "document"
|
|
1581
|
+
? `/page/${identity.id}`
|
|
1582
|
+
: identity.resourceType === "deck"
|
|
1583
|
+
? `/deck/${identity.id}`
|
|
1584
|
+
: identity.resourceType === "dashboard"
|
|
1585
|
+
? `/adhoc/${identity.id}`
|
|
1586
|
+
: identity.resourceType === "analysis"
|
|
1587
|
+
? `/analyses/${identity.id}`
|
|
1588
|
+
: identity.resourceType === "image"
|
|
1589
|
+
? `/image/${identity.id}`
|
|
1590
|
+
: identity.resourceType === "design"
|
|
1591
|
+
? `/design/${identity.id}`
|
|
1592
|
+
: undefined;
|
|
1593
|
+
return path ? artifactUrl(baseUrl, path) : undefined;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* Build a bounded participant-facing receipt from authenticated artifact writes.
|
|
1598
|
+
* Unlike generic artifact recovery, this only trusts identities extracted from
|
|
1599
|
+
* successful write actions (or a signed downstream write ledger), so a read or
|
|
1600
|
+
* an unverified URL cannot be rounded up to a successful mutation.
|
|
1601
|
+
*/
|
|
1602
|
+
export function buildA2AVerifiedMutationReceipt(
|
|
1603
|
+
toolResults: A2AToolResultSummary[],
|
|
1604
|
+
options: A2AArtifactResponseOptions = {},
|
|
1605
|
+
): string | null {
|
|
1606
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
1607
|
+
const identities = extractA2AArtifactIdentities(toolResults);
|
|
1608
|
+
if (identities.length === 0) return null;
|
|
1609
|
+
|
|
1610
|
+
const lines = identities.map((identity) => {
|
|
1611
|
+
const label =
|
|
1612
|
+
identity.resourceType.charAt(0).toUpperCase() +
|
|
1613
|
+
identity.resourceType.slice(1);
|
|
1614
|
+
const url = mutationReceiptUrl(identity, baseUrl);
|
|
1615
|
+
return url
|
|
1616
|
+
? `- ${label}: ${url} (ID: ${identity.id})`
|
|
1617
|
+
: `- ${label} ID: ${identity.id}`;
|
|
1618
|
+
});
|
|
1619
|
+
|
|
1620
|
+
return [
|
|
1621
|
+
"A verified change was saved, but I couldn't generate the detailed summary.",
|
|
1622
|
+
"",
|
|
1623
|
+
"Saved artifacts:",
|
|
1624
|
+
...lines,
|
|
1625
|
+
].join("\n");
|
|
1626
|
+
}
|
|
@@ -719,7 +719,7 @@ export function threadMessageTextForEngine(message: any): string {
|
|
|
719
719
|
|
|
720
720
|
const context = [
|
|
721
721
|
"<integration_artifact_context>",
|
|
722
|
-
"Trusted action history for this conversation. Resource IDs remain stable if participants rename the resource.
|
|
722
|
+
"Trusted action history for this conversation. Resource IDs remain stable if participants rename the resource. Fields such as titleAtAction are historical aliases from the time of that action, not current resource state. Use stable IDs to locate an earlier artifact, read its current state before changing it, and omit fields the user did not explicitly ask to change while still deciding whether to update, add, supersede, or create.",
|
|
723
723
|
promptSafeJson(artifacts),
|
|
724
724
|
"</integration_artifact_context>",
|
|
725
725
|
].join("\n");
|
|
@@ -2,6 +2,7 @@ import type { H3Event } from "h3";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
appendA2AArtifactLinks,
|
|
5
|
+
buildA2AVerifiedMutationReceipt,
|
|
5
6
|
extractA2AArtifactIdentities,
|
|
6
7
|
type A2AToolResultSummary,
|
|
7
8
|
} from "../a2a/artifact-response.js";
|
|
@@ -83,7 +84,13 @@ const DEFERRED_RESPONSE_MAX_HANDLER_MS = 2_500;
|
|
|
83
84
|
const EMPTY_INTEGRATION_RESPONSE_MESSAGE =
|
|
84
85
|
"The model finished without a visible answer. Try again, or open the thread in Dispatch to inspect the run.";
|
|
85
86
|
|
|
86
|
-
type ToolDoneEvent = {
|
|
87
|
+
type ToolDoneEvent = {
|
|
88
|
+
type: "tool_done";
|
|
89
|
+
tool: string;
|
|
90
|
+
result: string;
|
|
91
|
+
isError?: boolean;
|
|
92
|
+
completedSideEffect?: boolean;
|
|
93
|
+
};
|
|
87
94
|
|
|
88
95
|
/**
|
|
89
96
|
* Build a stable per-event dedup key from the incoming message. The same
|
|
@@ -204,6 +211,25 @@ function collectToolResultSummaries(
|
|
|
204
211
|
return completedRun.events
|
|
205
212
|
.map((runEvent) => runEvent.event)
|
|
206
213
|
.filter((event): event is ToolDoneEvent => event.type === "tool_done")
|
|
214
|
+
.map((event) => ({
|
|
215
|
+
tool: event.tool,
|
|
216
|
+
result: event.result,
|
|
217
|
+
isError: event.isError,
|
|
218
|
+
completedSideEffect: event.completedSideEffect,
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function collectCompletedMutationToolResultSummaries(
|
|
223
|
+
completedRun: ActiveRun,
|
|
224
|
+
): A2AToolResultSummary[] {
|
|
225
|
+
return completedRun.events
|
|
226
|
+
.map((runEvent) => runEvent.event)
|
|
227
|
+
.filter(
|
|
228
|
+
(event): event is ToolDoneEvent =>
|
|
229
|
+
event.type === "tool_done" &&
|
|
230
|
+
event.completedSideEffect === true &&
|
|
231
|
+
event.isError !== true,
|
|
232
|
+
)
|
|
207
233
|
.map((event) => ({ tool: event.tool, result: event.result }));
|
|
208
234
|
}
|
|
209
235
|
|
|
@@ -923,6 +949,18 @@ async function processIncomingMessage(
|
|
|
923
949
|
queuedA2AContinuation &&
|
|
924
950
|
isQueuedA2AContinuationDeferral(responseText);
|
|
925
951
|
|
|
952
|
+
// Compute trusted tool receipts before choosing the empty-answer
|
|
953
|
+
// fallback. A completed write must not be reported as though nothing
|
|
954
|
+
// happened merely because the model ran out of time before its prose
|
|
955
|
+
// summary. Read-only and unverified tool results do not qualify.
|
|
956
|
+
const baseUrl = process.env.APP_URL || process.env.URL || "";
|
|
957
|
+
const appBaseUrl = baseUrl ? withConfiguredAppBasePath(baseUrl) : "";
|
|
958
|
+
const toolResults = collectToolResultSummaries(completedRun);
|
|
959
|
+
const verifiedMutationReceipt = buildA2AVerifiedMutationReceipt(
|
|
960
|
+
collectCompletedMutationToolResultSummaries(completedRun),
|
|
961
|
+
{ baseUrl: appBaseUrl || undefined },
|
|
962
|
+
);
|
|
963
|
+
|
|
926
964
|
// If the run errored OR produced no text, post a graceful fallback so
|
|
927
965
|
// the user isn't left wondering whether the bot saw their message.
|
|
928
966
|
// Common case: an A2A delegation timed out and the agent loop bailed
|
|
@@ -953,7 +991,8 @@ async function processIncomingMessage(
|
|
|
953
991
|
"If it was a complex analytics question, opening the analytics app " +
|
|
954
992
|
"directly is the most reliable way to get an answer right now.";
|
|
955
993
|
} else {
|
|
956
|
-
responseText =
|
|
994
|
+
responseText =
|
|
995
|
+
verifiedMutationReceipt ?? EMPTY_INTEGRATION_RESPONSE_MESSAGE;
|
|
957
996
|
}
|
|
958
997
|
}
|
|
959
998
|
if (approval?.type === "approval_required") {
|
|
@@ -965,9 +1004,6 @@ async function processIncomingMessage(
|
|
|
965
1004
|
// platforms with rich blocks (Slack) can render a button instead
|
|
966
1005
|
// of inlining a `<url|text>` link that auto-unfurls into a giant
|
|
967
1006
|
// preview card.
|
|
968
|
-
const baseUrl = process.env.APP_URL || process.env.URL || "";
|
|
969
|
-
const appBaseUrl = baseUrl ? withConfiguredAppBasePath(baseUrl) : "";
|
|
970
|
-
const toolResults = collectToolResultSummaries(completedRun);
|
|
971
1007
|
if (!suppressPlatformReply) {
|
|
972
1008
|
responseText = appendA2AArtifactLinks(responseText, toolResults, {
|
|
973
1009
|
baseUrl: appBaseUrl || undefined,
|
|
@@ -13,7 +13,10 @@ import {
|
|
|
13
13
|
type H3Event,
|
|
14
14
|
} from "h3";
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
buildA2ARecoverableArtifactMessage,
|
|
18
|
+
type A2AToolResultSummary,
|
|
19
|
+
} from "../a2a/artifact-response.js";
|
|
17
20
|
import {
|
|
18
21
|
hasConfiguredA2ASecret,
|
|
19
22
|
isLoopbackAddress,
|
|
@@ -1463,7 +1466,7 @@ export function createAgentChatPlugin(
|
|
|
1463
1466
|
// Run the SAME agent loop, then extract the final answer from the
|
|
1464
1467
|
// event stream so pre-tool narration never leaks as the A2A result.
|
|
1465
1468
|
const a2aEvents: AgentChatEvent[] = [];
|
|
1466
|
-
const a2aToolResults:
|
|
1469
|
+
const a2aToolResults: A2AToolResultSummary[] = [];
|
|
1467
1470
|
let lastRecoverableArtifactText = "";
|
|
1468
1471
|
const controller = new AbortController();
|
|
1469
1472
|
|
|
@@ -1498,6 +1501,8 @@ export function createAgentChatPlugin(
|
|
|
1498
1501
|
a2aToolResults.push({
|
|
1499
1502
|
tool: event.tool,
|
|
1500
1503
|
result: event.result,
|
|
1504
|
+
isError: event.isError,
|
|
1505
|
+
completedSideEffect: event.completedSideEffect,
|
|
1501
1506
|
});
|
|
1502
1507
|
const recoverableArtifactText =
|
|
1503
1508
|
buildA2ARecoverableArtifactMessage(a2aToolResults, {
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
IconDeviceDesktop,
|
|
8
8
|
IconExternalLink,
|
|
9
9
|
} from "@tabler/icons-react";
|
|
10
|
-
import { type ReactNode } from "react";
|
|
10
|
+
import { type ReactNode, useSyncExternalStore } from "react";
|
|
11
11
|
|
|
12
12
|
import { Button, type ButtonProps } from "@/components/ui/button";
|
|
13
13
|
import {
|
|
@@ -16,11 +16,24 @@ import {
|
|
|
16
16
|
PopoverTrigger,
|
|
17
17
|
} from "@/components/ui/popover";
|
|
18
18
|
import {
|
|
19
|
+
attemptOpenDesktopApp,
|
|
19
20
|
clipsChromeExtensionEnabled,
|
|
20
21
|
clipsChromeExtensionUrl,
|
|
22
|
+
hasDownloadedDesktopApp,
|
|
23
|
+
subscribeDownloaded,
|
|
21
24
|
} from "@/lib/capture-install-options";
|
|
22
25
|
import { cn } from "@/lib/utils";
|
|
23
26
|
|
|
27
|
+
// SSR snapshot is always false; same-tab markDesktopAppDownloaded() notifies
|
|
28
|
+
// subscribers so mounted CTAs flip to "Open" without a reload.
|
|
29
|
+
function useHasDownloadedDesktopApp(): boolean {
|
|
30
|
+
return useSyncExternalStore(
|
|
31
|
+
subscribeDownloaded,
|
|
32
|
+
hasDownloadedDesktopApp,
|
|
33
|
+
() => false,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
type PopoverPlacement = {
|
|
25
38
|
align?: "start" | "center" | "end";
|
|
26
39
|
side?: "top" | "right" | "bottom" | "left";
|
|
@@ -29,11 +42,15 @@ type PopoverPlacement = {
|
|
|
29
42
|
type CaptureInstallButtonProps = Omit<ButtonProps, "asChild"> &
|
|
30
43
|
PopoverPlacement & {
|
|
31
44
|
children: ReactNode;
|
|
45
|
+
/** Label shown once the desktop app has been downloaded. */
|
|
46
|
+
downloadedChildren?: ReactNode;
|
|
32
47
|
desktopHref?: string;
|
|
33
48
|
};
|
|
34
49
|
|
|
35
50
|
type CaptureInstallInlineLinkProps = PopoverPlacement & {
|
|
36
51
|
children: ReactNode;
|
|
52
|
+
/** Label shown once the desktop app has been downloaded. */
|
|
53
|
+
downloadedChildren?: ReactNode;
|
|
37
54
|
className?: string;
|
|
38
55
|
desktopHref?: string;
|
|
39
56
|
};
|
|
@@ -110,16 +127,37 @@ function InstallOptionsContent({ desktopHref = "/download" }) {
|
|
|
110
127
|
|
|
111
128
|
export function CaptureInstallButton({
|
|
112
129
|
children,
|
|
130
|
+
downloadedChildren,
|
|
113
131
|
className,
|
|
114
132
|
desktopHref = "/download",
|
|
115
133
|
align = "end",
|
|
116
134
|
side = "bottom",
|
|
117
135
|
...buttonProps
|
|
118
136
|
}: CaptureInstallButtonProps) {
|
|
137
|
+
const downloaded = useHasDownloadedDesktopApp();
|
|
138
|
+
const label = downloaded ? (downloadedChildren ?? children) : children;
|
|
139
|
+
|
|
140
|
+
if (downloaded) {
|
|
141
|
+
const { onClick, ...restButtonProps } = buttonProps;
|
|
142
|
+
return (
|
|
143
|
+
<Button
|
|
144
|
+
className={className}
|
|
145
|
+
{...restButtonProps}
|
|
146
|
+
onClick={(event) => {
|
|
147
|
+
onClick?.(event);
|
|
148
|
+
if (event.defaultPrevented) return;
|
|
149
|
+
attemptOpenDesktopApp(desktopHref);
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
{label}
|
|
153
|
+
</Button>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
119
157
|
if (!clipsChromeExtensionEnabled) {
|
|
120
158
|
return (
|
|
121
159
|
<Button asChild className={className} {...buttonProps}>
|
|
122
|
-
<a href={appPath(desktopHref)}>{
|
|
160
|
+
<a href={appPath(desktopHref)}>{label}</a>
|
|
123
161
|
</Button>
|
|
124
162
|
);
|
|
125
163
|
}
|
|
@@ -128,7 +166,7 @@ export function CaptureInstallButton({
|
|
|
128
166
|
<Popover>
|
|
129
167
|
<PopoverTrigger asChild>
|
|
130
168
|
<Button className={className} {...buttonProps}>
|
|
131
|
-
{
|
|
169
|
+
{label}
|
|
132
170
|
<IconChevronDown className="h-3.5 w-3.5" />
|
|
133
171
|
</Button>
|
|
134
172
|
</PopoverTrigger>
|
|
@@ -141,15 +179,32 @@ export function CaptureInstallButton({
|
|
|
141
179
|
|
|
142
180
|
export function CaptureInstallInlineLink({
|
|
143
181
|
children,
|
|
182
|
+
downloadedChildren,
|
|
144
183
|
className,
|
|
145
184
|
desktopHref = "/download",
|
|
146
185
|
align = "start",
|
|
147
186
|
side = "bottom",
|
|
148
187
|
}: CaptureInstallInlineLinkProps) {
|
|
188
|
+
const downloaded = useHasDownloadedDesktopApp();
|
|
189
|
+
|
|
190
|
+
const label = downloaded ? (downloadedChildren ?? children) : children;
|
|
191
|
+
|
|
192
|
+
if (downloaded) {
|
|
193
|
+
return (
|
|
194
|
+
<button
|
|
195
|
+
type="button"
|
|
196
|
+
onClick={() => attemptOpenDesktopApp(desktopHref)}
|
|
197
|
+
className={cn("cursor-pointer", className)}
|
|
198
|
+
>
|
|
199
|
+
{label}
|
|
200
|
+
</button>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
149
204
|
if (!clipsChromeExtensionEnabled) {
|
|
150
205
|
return (
|
|
151
206
|
<a href={appPath(desktopHref)} className={className}>
|
|
152
|
-
{
|
|
207
|
+
{label}
|
|
153
208
|
</a>
|
|
154
209
|
);
|
|
155
210
|
}
|
|
@@ -158,7 +213,7 @@ export function CaptureInstallInlineLink({
|
|
|
158
213
|
<Popover>
|
|
159
214
|
<PopoverTrigger asChild>
|
|
160
215
|
<button type="button" className={cn("cursor-pointer", className)}>
|
|
161
|
-
{
|
|
216
|
+
{label}
|
|
162
217
|
</button>
|
|
163
218
|
</PopoverTrigger>
|
|
164
219
|
<PopoverContent align={align} side={side} className="w-80 p-3">
|
|
@@ -528,7 +528,15 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
|
|
|
528
528
|
<>
|
|
529
529
|
<div className="shrink-0 space-y-1.5 px-2 py-1.5">
|
|
530
530
|
{shouldShowSidebarLink && (
|
|
531
|
-
<CaptureInstallInlineLink
|
|
531
|
+
<CaptureInstallInlineLink
|
|
532
|
+
className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-xs text-foreground hover:bg-accent/60"
|
|
533
|
+
downloadedChildren={
|
|
534
|
+
<>
|
|
535
|
+
<IconAppWindow className="h-4 w-4" />
|
|
536
|
+
{t("captureInstall.openDesktopApp")}
|
|
537
|
+
</>
|
|
538
|
+
}
|
|
539
|
+
>
|
|
532
540
|
<IconAppWindow className="h-4 w-4" />
|
|
533
541
|
{t("navigation.desktopCta")}
|
|
534
542
|
</CaptureInstallInlineLink>
|
|
@@ -2,8 +2,8 @@ import { useCallback, useEffect, useState } from "react";
|
|
|
2
2
|
|
|
3
3
|
import { useIsMobile } from "@/hooks/use-mobile";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
hasDismissedDesktopPromo,
|
|
6
|
+
markDesktopPromoDismissed,
|
|
7
7
|
} from "@/lib/capture-install-options";
|
|
8
8
|
|
|
9
9
|
function detectDesktopApp(): boolean {
|
|
@@ -27,12 +27,12 @@ export function useDesktopPromo() {
|
|
|
27
27
|
|
|
28
28
|
useEffect(() => {
|
|
29
29
|
setIsDesktopApp(detectDesktopApp());
|
|
30
|
-
setDismissed(
|
|
30
|
+
setDismissed(hasDismissedDesktopPromo());
|
|
31
31
|
}, []);
|
|
32
32
|
|
|
33
33
|
const dismiss = useCallback(() => {
|
|
34
34
|
setDismissed(true);
|
|
35
|
-
|
|
35
|
+
markDesktopPromoDismissed();
|
|
36
36
|
}, []);
|
|
37
37
|
|
|
38
38
|
return {
|
|
@@ -1106,6 +1106,7 @@ const messages = {
|
|
|
1106
1106
|
desktopTitle: "Desktop app (مترجم)",
|
|
1107
1107
|
desktopDescription:
|
|
1108
1108
|
"Most seamless for global shortcuts, menu-bar recording, meetings, and repeat captures. (مترجم)",
|
|
1109
|
+
openDesktopApp: "Open desktop app (مترجم)",
|
|
1109
1110
|
},
|
|
1110
1111
|
editableTitle: {
|
|
1111
1112
|
untitled: "Untitled Clip (مترجم)",
|
|
@@ -1132,6 +1132,7 @@ Alle sichtbaren Änderungen für Clips-Nutzer werden hier dokumentiert. Du kanns
|
|
|
1132
1132
|
desktopTitle: "Desktop app (Lokalisiert)",
|
|
1133
1133
|
desktopDescription:
|
|
1134
1134
|
"Most seamless for global shortcuts, menu-bar recording, meetings, and repeat captures. (Lokalisiert)",
|
|
1135
|
+
openDesktopApp: "Open desktop app (Lokalisiert)",
|
|
1135
1136
|
},
|
|
1136
1137
|
editableTitle: {
|
|
1137
1138
|
untitled: "Untitled Clip (Lokalisiert)",
|
|
@@ -1102,6 +1102,7 @@ All notable user-facing changes to Clips are documented here. Open it any time f
|
|
|
1102
1102
|
desktopTitle: "Desktop app",
|
|
1103
1103
|
desktopDescription:
|
|
1104
1104
|
"Most seamless for global shortcuts, menu-bar recording, meetings, and repeat captures.",
|
|
1105
|
+
openDesktopApp: "Open desktop app",
|
|
1105
1106
|
},
|
|
1106
1107
|
editableTitle: {
|
|
1107
1108
|
untitled: "Untitled Clip",
|
|
@@ -1125,6 +1125,7 @@ Todos los cambios visibles para los usuarios de Clips se documentan aquí. Puede
|
|
|
1125
1125
|
desktopTitle: "Desktop app (Localizado)",
|
|
1126
1126
|
desktopDescription:
|
|
1127
1127
|
"Most seamless for global shortcuts, menu-bar recording, meetings, and repeat captures. (Localizado)",
|
|
1128
|
+
openDesktopApp: "Open desktop app (Localizado)",
|
|
1128
1129
|
},
|
|
1129
1130
|
editableTitle: {
|
|
1130
1131
|
untitled: "Untitled Clip (Localizado)",
|
|
@@ -1127,6 +1127,7 @@ Tous les changements visibles par les utilisateurs de Clips sont documentés ici
|
|
|
1127
1127
|
desktopTitle: "Desktop app (Localisé)",
|
|
1128
1128
|
desktopDescription:
|
|
1129
1129
|
"Most seamless for global shortcuts, menu-bar recording, meetings, and repeat captures. (Localisé)",
|
|
1130
|
+
openDesktopApp: "Open desktop app (Localisé)",
|
|
1130
1131
|
},
|
|
1131
1132
|
editableTitle: {
|
|
1132
1133
|
untitled: "Untitled Clip (Localisé)",
|
|
@@ -1093,6 +1093,7 @@ Clips में उपयोगकर्ताओं को दिखने व
|
|
|
1093
1093
|
desktopTitle: "Desktop app (स्थानीयकृत)",
|
|
1094
1094
|
desktopDescription:
|
|
1095
1095
|
"Most seamless for global shortcuts, menu-bar recording, meetings, and repeat captures. (स्थानीयकृत)",
|
|
1096
|
+
openDesktopApp: "Open desktop app (स्थानीयकृत)",
|
|
1096
1097
|
},
|
|
1097
1098
|
editableTitle: {
|
|
1098
1099
|
untitled: "Untitled Clip (स्थानीयकृत)",
|
|
@@ -1114,6 +1114,7 @@ Clips のユーザー向けの主な変更はここに記録されます。コ
|
|
|
1114
1114
|
desktopTitle: "Desktop app (ローカライズ済み)",
|
|
1115
1115
|
desktopDescription:
|
|
1116
1116
|
"Most seamless for global shortcuts, menu-bar recording, meetings, and repeat captures. (ローカライズ済み)",
|
|
1117
|
+
openDesktopApp: "Open desktop app (ローカライズ済み)",
|
|
1117
1118
|
},
|
|
1118
1119
|
editableTitle: {
|
|
1119
1120
|
untitled: "Untitled Clip (ローカライズ済み)",
|
|
@@ -1103,6 +1103,7 @@ Clips의 모든 사용자 대상 변경 사항은 여기에 기록됩니다. 명
|
|
|
1103
1103
|
desktopTitle: "Desktop app (현지화됨)",
|
|
1104
1104
|
desktopDescription:
|
|
1105
1105
|
"Most seamless for global shortcuts, menu-bar recording, meetings, and repeat captures. (현지화됨)",
|
|
1106
|
+
openDesktopApp: "Open desktop app (현지화됨)",
|
|
1106
1107
|
},
|
|
1107
1108
|
editableTitle: {
|
|
1108
1109
|
untitled: "Untitled Clip (현지화됨)",
|
|
@@ -1121,6 +1121,7 @@ Todas as mudanças visíveis para usuários do Clips são documentadas aqui. Voc
|
|
|
1121
1121
|
desktopTitle: "Desktop app (Localizado)",
|
|
1122
1122
|
desktopDescription:
|
|
1123
1123
|
"Most seamless for global shortcuts, menu-bar recording, meetings, and repeat captures. (Localizado)",
|
|
1124
|
+
openDesktopApp: "Open desktop app (Localizado)",
|
|
1124
1125
|
},
|
|
1125
1126
|
editableTitle: {
|
|
1126
1127
|
untitled: "Untitled Clip (Localizado)",
|
|
@@ -1064,6 +1064,7 @@ Clips 中所有面向用户的重要更改都会记录在这里。你可以随
|
|
|
1064
1064
|
desktopTitle: "Desktop app (已本地化)",
|
|
1065
1065
|
desktopDescription:
|
|
1066
1066
|
"Most seamless for global shortcuts, menu-bar recording, meetings, and repeat captures. (已本地化)",
|
|
1067
|
+
openDesktopApp: "Open desktop app (已本地化)",
|
|
1067
1068
|
},
|
|
1068
1069
|
editableTitle: {
|
|
1069
1070
|
untitled: "Untitled Clip (已本地化)",
|