@agent-native/core 0.106.1 → 0.106.3
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 +12 -0
- package/corpus/core/docs/content/external-agents.mdx +8 -0
- package/corpus/core/docs/content/locales/ar-SA/external-agents.mdx +7 -0
- package/corpus/core/docs/content/locales/ar-SA/mcp-protocol.mdx +7 -0
- package/corpus/core/docs/content/locales/de-DE/external-agents.mdx +9 -0
- package/corpus/core/docs/content/locales/de-DE/mcp-protocol.mdx +9 -0
- package/corpus/core/docs/content/locales/es-ES/external-agents.mdx +9 -0
- package/corpus/core/docs/content/locales/es-ES/mcp-protocol.mdx +9 -0
- package/corpus/core/docs/content/locales/fr-FR/external-agents.mdx +9 -0
- package/corpus/core/docs/content/locales/fr-FR/mcp-protocol.mdx +9 -0
- package/corpus/core/docs/content/locales/hi-IN/external-agents.mdx +8 -0
- package/corpus/core/docs/content/locales/hi-IN/mcp-protocol.mdx +8 -0
- package/corpus/core/docs/content/locales/ja-JP/external-agents.mdx +8 -0
- package/corpus/core/docs/content/locales/ja-JP/mcp-protocol.mdx +8 -0
- package/corpus/core/docs/content/locales/ko-KR/external-agents.mdx +8 -0
- package/corpus/core/docs/content/locales/ko-KR/mcp-protocol.mdx +8 -0
- package/corpus/core/docs/content/locales/pt-BR/external-agents.mdx +9 -0
- package/corpus/core/docs/content/locales/pt-BR/mcp-protocol.mdx +9 -0
- package/corpus/core/docs/content/locales/zh-CN/external-agents.mdx +6 -0
- package/corpus/core/docs/content/locales/zh-CN/mcp-protocol.mdx +6 -0
- package/corpus/core/docs/content/locales/zh-TW/external-agents.mdx +6 -0
- package/corpus/core/docs/content/locales/zh-TW/mcp-protocol.mdx +6 -0
- package/corpus/core/docs/content/mcp-protocol.mdx +8 -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/mcp/builtin-tools.ts +115 -10
- 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/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/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +87 -6
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +1 -1
- 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/agent-engine-api-key-route.d.ts +1 -1
- package/docs/content/external-agents.mdx +8 -0
- package/docs/content/locales/ar-SA/external-agents.mdx +7 -0
- package/docs/content/locales/ar-SA/mcp-protocol.mdx +7 -0
- package/docs/content/locales/de-DE/external-agents.mdx +9 -0
- package/docs/content/locales/de-DE/mcp-protocol.mdx +9 -0
- package/docs/content/locales/es-ES/external-agents.mdx +9 -0
- package/docs/content/locales/es-ES/mcp-protocol.mdx +9 -0
- package/docs/content/locales/fr-FR/external-agents.mdx +9 -0
- package/docs/content/locales/fr-FR/mcp-protocol.mdx +9 -0
- package/docs/content/locales/hi-IN/external-agents.mdx +8 -0
- package/docs/content/locales/hi-IN/mcp-protocol.mdx +8 -0
- package/docs/content/locales/ja-JP/external-agents.mdx +8 -0
- package/docs/content/locales/ja-JP/mcp-protocol.mdx +8 -0
- package/docs/content/locales/ko-KR/external-agents.mdx +8 -0
- package/docs/content/locales/ko-KR/mcp-protocol.mdx +8 -0
- package/docs/content/locales/pt-BR/external-agents.mdx +9 -0
- package/docs/content/locales/pt-BR/mcp-protocol.mdx +9 -0
- package/docs/content/locales/zh-CN/external-agents.mdx +6 -0
- package/docs/content/locales/zh-CN/mcp-protocol.mdx +6 -0
- package/docs/content/locales/zh-TW/external-agents.mdx +6 -0
- package/docs/content/locales/zh-TW/mcp-protocol.mdx +6 -0
- package/docs/content/mcp-protocol.mdx +8 -0
- package/package.json +1 -1
|
@@ -123,8 +123,18 @@ interface AskAppTaskResult {
|
|
|
123
123
|
arguments: { app: string; taskId: string };
|
|
124
124
|
};
|
|
125
125
|
message?: string;
|
|
126
|
+
statusRead?: "unavailable";
|
|
127
|
+
retryable?: true;
|
|
128
|
+
errorCategory?: AskAppStatusErrorCategory;
|
|
129
|
+
attempts?: number;
|
|
126
130
|
}
|
|
127
131
|
|
|
132
|
+
type AskAppStatusErrorCategory =
|
|
133
|
+
| "transport"
|
|
134
|
+
| "timeout"
|
|
135
|
+
| "upstream_5xx"
|
|
136
|
+
| "rate_limited";
|
|
137
|
+
|
|
128
138
|
function safeAppPath(raw: unknown): string | null {
|
|
129
139
|
if (typeof raw !== "string" || !raw.trim()) return null;
|
|
130
140
|
const value = raw.trim();
|
|
@@ -432,34 +442,125 @@ async function fetchAskAppA2ATask(
|
|
|
432
442
|
taskId: string,
|
|
433
443
|
): Promise<AskAppTaskResult> {
|
|
434
444
|
const { client } = await createA2AClientForAskApp(route.origin);
|
|
435
|
-
|
|
445
|
+
const maxAttempts = ASK_APP_STATUS_RETRY_DELAYS_MS.length + 1;
|
|
436
446
|
for (
|
|
437
447
|
let attempt = 0;
|
|
438
448
|
attempt <= ASK_APP_STATUS_RETRY_DELAYS_MS.length;
|
|
439
449
|
attempt++
|
|
440
450
|
) {
|
|
451
|
+
const startedAt = Date.now();
|
|
441
452
|
try {
|
|
442
453
|
const task = await client.getTask(taskId);
|
|
443
454
|
return askAppTaskResult(route, task);
|
|
444
455
|
} catch (err) {
|
|
445
|
-
lastError = err;
|
|
446
456
|
const delayMs = ASK_APP_STATUS_RETRY_DELAYS_MS[attempt];
|
|
447
|
-
|
|
457
|
+
const errorCategory = askAppStatusErrorCategory(err);
|
|
458
|
+
const retryable = errorCategory != null;
|
|
459
|
+
const willRetry = retryable && delayMs != null;
|
|
460
|
+
if (retryable) {
|
|
461
|
+
console.warn("[ask_app_status] tasks/get attempt failed", {
|
|
462
|
+
app: route.app,
|
|
463
|
+
routedVia: route.routedVia,
|
|
464
|
+
taskId,
|
|
465
|
+
originHost: askAppStatusOriginHost(route.origin),
|
|
466
|
+
attempt: attempt + 1,
|
|
467
|
+
maxAttempts,
|
|
468
|
+
elapsedMs: Date.now() - startedAt,
|
|
469
|
+
errorCategory,
|
|
470
|
+
errorName: err instanceof Error ? err.name : typeof err,
|
|
471
|
+
causeCode: askAppStatusErrorCauseCode(err),
|
|
472
|
+
willRetry,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (!retryable) {
|
|
448
477
|
throw err;
|
|
449
478
|
}
|
|
479
|
+
if (delayMs == null) {
|
|
480
|
+
return askAppStatusReadUnavailableResult(
|
|
481
|
+
route,
|
|
482
|
+
taskId,
|
|
483
|
+
errorCategory,
|
|
484
|
+
maxAttempts,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
450
487
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
451
488
|
}
|
|
452
489
|
}
|
|
453
|
-
throw
|
|
490
|
+
throw new Error("ask_app_status retry loop exited unexpectedly.");
|
|
454
491
|
}
|
|
455
492
|
|
|
456
493
|
function isTransientAskAppStatusError(err: unknown): boolean {
|
|
494
|
+
return askAppStatusErrorCategory(err) != null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function askAppStatusErrorCategory(
|
|
498
|
+
err: unknown,
|
|
499
|
+
): AskAppStatusErrorCategory | null {
|
|
457
500
|
const message = err instanceof Error ? err.message : String(err ?? "");
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
501
|
+
const causeCode = askAppStatusErrorCauseCode(err) ?? "";
|
|
502
|
+
const diagnostic = `${message} ${causeCode}`;
|
|
503
|
+
if (/A2A request failed \(429\)/i.test(message)) return "rate_limited";
|
|
504
|
+
if (/A2A request failed \((?:500|502|503|504)\)/i.test(message)) {
|
|
505
|
+
return "upstream_5xx";
|
|
506
|
+
}
|
|
507
|
+
if (/etimedout|timeout|aborted|aborterror/i.test(diagnostic)) {
|
|
508
|
+
return "timeout";
|
|
509
|
+
}
|
|
510
|
+
if (
|
|
511
|
+
/\bfetch failed\b|failed to fetch|networkerror|socket hang up|econnreset/i.test(
|
|
512
|
+
diagnostic,
|
|
513
|
+
)
|
|
514
|
+
) {
|
|
515
|
+
return "transport";
|
|
516
|
+
}
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function askAppStatusErrorCauseCode(err: unknown): string | undefined {
|
|
521
|
+
if (!(err instanceof Error)) return undefined;
|
|
522
|
+
const directCode = (err as Error & { code?: unknown }).code;
|
|
523
|
+
if (typeof directCode === "string" && directCode.trim()) {
|
|
524
|
+
return directCode.trim();
|
|
525
|
+
}
|
|
526
|
+
if (!err.cause || typeof err.cause !== "object") return undefined;
|
|
527
|
+
const code = (err.cause as { code?: unknown }).code;
|
|
528
|
+
return typeof code === "string" && code.trim() ? code.trim() : undefined;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function askAppStatusOriginHost(origin: string): string {
|
|
532
|
+
try {
|
|
533
|
+
return new URL(origin).host;
|
|
534
|
+
} catch {
|
|
535
|
+
return "unknown";
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function askAppStatusReadUnavailableResult(
|
|
540
|
+
route: AskAppRoute,
|
|
541
|
+
taskId: string,
|
|
542
|
+
errorCategory: AskAppStatusErrorCategory,
|
|
543
|
+
attempts: number,
|
|
544
|
+
): AskAppTaskResult {
|
|
545
|
+
return {
|
|
546
|
+
app: route.app,
|
|
547
|
+
routedVia: route.routedVia,
|
|
548
|
+
taskId,
|
|
549
|
+
status: "unknown",
|
|
550
|
+
statusRead: "unavailable",
|
|
551
|
+
retryable: true,
|
|
552
|
+
errorCategory,
|
|
553
|
+
attempts,
|
|
554
|
+
pollAfterMs: ASK_APP_POLL_INTERVAL_MS,
|
|
555
|
+
poll: {
|
|
556
|
+
tool: "ask_app_status",
|
|
557
|
+
arguments: { app: route.app, taskId },
|
|
558
|
+
},
|
|
559
|
+
message:
|
|
560
|
+
"The durable ask_app task status could not be read after bounded retries. " +
|
|
561
|
+
"The task may still be running or completed. Retry ask_app_status " +
|
|
562
|
+
"with the same app and taskId; do not resubmit ask_app.",
|
|
563
|
+
};
|
|
463
564
|
}
|
|
464
565
|
|
|
465
566
|
/**
|
|
@@ -1186,7 +1287,11 @@ function askAppStatusTool(
|
|
|
1186
1287
|
): ActionEntry {
|
|
1187
1288
|
return {
|
|
1188
1289
|
tool: tool(
|
|
1189
|
-
"Poll a durable ask_app task and return its current status or final response."
|
|
1290
|
+
"Poll a durable ask_app task and return its current status or final response. " +
|
|
1291
|
+
"If a transient status read stays unavailable after bounded retries, the " +
|
|
1292
|
+
"result preserves the app and taskId with statusRead 'unavailable' and a " +
|
|
1293
|
+
"poll instruction. Retry ask_app_status for that same task; never resubmit " +
|
|
1294
|
+
"ask_app as status-read recovery because that can duplicate work.",
|
|
1190
1295
|
{
|
|
1191
1296
|
app: {
|
|
1192
1297
|
type: "string",
|
|
@@ -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 (已本地化)",
|
|
@@ -1,22 +1,107 @@
|
|
|
1
|
-
|
|
1
|
+
import { appPath } from "@agent-native/core/client";
|
|
2
|
+
|
|
3
|
+
const DESKTOP_PROMO_DISMISSED_STORAGE_KEY = "clips.desktop-promo.dismissed";
|
|
4
|
+
const DESKTOP_DOWNLOADED_STORAGE_KEY = "clips.desktop-app.downloaded";
|
|
5
|
+
|
|
6
|
+
// Custom scheme the desktop build registers. A web click tries this first and
|
|
7
|
+
// falls back to the download page when nothing handles it.
|
|
8
|
+
const DESKTOP_APP_PROTOCOL_URL = "clips://open";
|
|
9
|
+
const DESKTOP_APP_LAUNCH_FALLBACK_MS = 800;
|
|
2
10
|
|
|
3
11
|
export function hasDownloadedDesktopApp(): boolean {
|
|
4
12
|
try {
|
|
13
|
+
if (typeof window === "undefined") return false;
|
|
14
|
+
const ls = window.localStorage;
|
|
15
|
+
// Also treat the legacy dismissed flag as "downloaded" — before the flag
|
|
16
|
+
// split, a single key covered both states, so existing users only have it.
|
|
5
17
|
return (
|
|
6
|
-
|
|
7
|
-
|
|
18
|
+
ls?.getItem(DESKTOP_DOWNLOADED_STORAGE_KEY) === "1" ||
|
|
19
|
+
ls?.getItem(DESKTOP_PROMO_DISMISSED_STORAGE_KEY) === "1"
|
|
8
20
|
);
|
|
9
21
|
} catch {
|
|
10
22
|
return false;
|
|
11
23
|
}
|
|
12
24
|
}
|
|
13
25
|
|
|
26
|
+
const downloadedListeners = new Set<() => void>();
|
|
27
|
+
|
|
28
|
+
export function subscribeDownloaded(callback: () => void): () => void {
|
|
29
|
+
downloadedListeners.add(callback);
|
|
30
|
+
return () => {
|
|
31
|
+
downloadedListeners.delete(callback);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
14
35
|
export function markDesktopAppDownloaded(): void {
|
|
15
36
|
try {
|
|
16
|
-
|
|
37
|
+
// Downloading (or successfully launching) the app also hides the promo.
|
|
38
|
+
window.localStorage?.setItem(DESKTOP_DOWNLOADED_STORAGE_KEY, "1");
|
|
39
|
+
window.localStorage?.setItem(DESKTOP_PROMO_DISMISSED_STORAGE_KEY, "1");
|
|
17
40
|
} catch {
|
|
18
41
|
// Download tracking is best-effort and must not block the installer.
|
|
19
42
|
}
|
|
43
|
+
downloadedListeners.forEach((fn) => fn());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function hasDismissedDesktopPromo(): boolean {
|
|
47
|
+
try {
|
|
48
|
+
return (
|
|
49
|
+
typeof window !== "undefined" &&
|
|
50
|
+
window.localStorage?.getItem(DESKTOP_PROMO_DISMISSED_STORAGE_KEY) === "1"
|
|
51
|
+
);
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function markDesktopPromoDismissed(): void {
|
|
58
|
+
try {
|
|
59
|
+
window.localStorage?.setItem(DESKTOP_PROMO_DISMISSED_STORAGE_KEY, "1");
|
|
60
|
+
} catch {
|
|
61
|
+
// Dismiss tracking is best-effort.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Try to launch the installed desktop app via its custom protocol, falling back
|
|
67
|
+
* to the download page when nothing handles the scheme. Browsers expose no way
|
|
68
|
+
* to query whether the protocol is registered, so we watch for the tab losing
|
|
69
|
+
* focus (the app taking over) within a short window; if that never happens we
|
|
70
|
+
* assume the app is not installed and navigate to the fallback. A successful
|
|
71
|
+
* launch self-heals the stored "downloaded" flag.
|
|
72
|
+
*/
|
|
73
|
+
export function attemptOpenDesktopApp(fallbackHref = "/download"): void {
|
|
74
|
+
if (typeof window === "undefined") return;
|
|
75
|
+
const fallbackUrl = appPath(fallbackHref);
|
|
76
|
+
|
|
77
|
+
let launched = false;
|
|
78
|
+
const cleanup = () => {
|
|
79
|
+
window.removeEventListener("blur", onLeave);
|
|
80
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
81
|
+
};
|
|
82
|
+
const onLeave = () => {
|
|
83
|
+
if (launched) return;
|
|
84
|
+
launched = true;
|
|
85
|
+
markDesktopAppDownloaded();
|
|
86
|
+
cleanup();
|
|
87
|
+
};
|
|
88
|
+
const onVisibility = () => {
|
|
89
|
+
if (document.hidden) onLeave();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
window.addEventListener("blur", onLeave);
|
|
93
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
94
|
+
|
|
95
|
+
window.setTimeout(() => {
|
|
96
|
+
cleanup();
|
|
97
|
+
if (!launched) window.location.href = fallbackUrl;
|
|
98
|
+
}, DESKTOP_APP_LAUNCH_FALLBACK_MS);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
window.location.href = DESKTOP_APP_PROTOCOL_URL;
|
|
102
|
+
} catch {
|
|
103
|
+
// Some browsers throw on an unknown scheme; the timeout handles fallback.
|
|
104
|
+
}
|
|
20
105
|
}
|
|
21
106
|
|
|
22
107
|
function isFalsy(value: unknown): boolean {
|
|
@@ -640,7 +640,16 @@ function EmptyState({ isDesktopApp }: { isDesktopApp: boolean }) {
|
|
|
640
640
|
{t("dictateRoute.emptyWebDescription")}
|
|
641
641
|
</p>
|
|
642
642
|
<div className="mt-5 flex items-center justify-center">
|
|
643
|
-
<CaptureInstallButton
|
|
643
|
+
<CaptureInstallButton
|
|
644
|
+
size="sm"
|
|
645
|
+
className="gap-1.5"
|
|
646
|
+
downloadedChildren={
|
|
647
|
+
<>
|
|
648
|
+
<IconDeviceDesktop className="h-3.5 w-3.5" />
|
|
649
|
+
{t("captureInstall.openDesktopApp")}
|
|
650
|
+
</>
|
|
651
|
+
}
|
|
652
|
+
>
|
|
644
653
|
<IconDownload className="h-3.5 w-3.5" />
|
|
645
654
|
{t("dictateRoute.downloadDesktopApp")}
|
|
646
655
|
</CaptureInstallButton>
|
|
@@ -670,6 +670,7 @@ function DesktopRecorderCallout() {
|
|
|
670
670
|
<CaptureInstallButton
|
|
671
671
|
size="sm"
|
|
672
672
|
className="mt-4 w-full bg-primary text-primary-foreground hover:bg-primary/90"
|
|
673
|
+
downloadedChildren={t("captureInstall.openDesktopApp")}
|
|
673
674
|
>
|
|
674
675
|
{t("recordRoute.downloadDesktopApp")}
|
|
675
676
|
</CaptureInstallButton>
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
IconAlertTriangle,
|
|
13
13
|
IconArrowLeft,
|
|
14
|
+
IconDeviceDesktop,
|
|
14
15
|
IconDownload,
|
|
15
16
|
IconDots,
|
|
16
17
|
IconExternalLink,
|
|
@@ -1220,6 +1221,12 @@ function PublicAgentEmptyState({
|
|
|
1220
1221
|
className="w-full gap-2"
|
|
1221
1222
|
align="center"
|
|
1222
1223
|
onClick={() => onCtaClick("download")}
|
|
1224
|
+
downloadedChildren={
|
|
1225
|
+
<>
|
|
1226
|
+
<IconDeviceDesktop className="h-4 w-4" />
|
|
1227
|
+
{t("captureInstall.openDesktopApp")}
|
|
1228
|
+
</>
|
|
1229
|
+
}
|
|
1223
1230
|
>
|
|
1224
1231
|
<IconDownload className="h-4 w-4" />
|
|
1225
1232
|
{downloadLabel}
|
|
@@ -34,6 +34,11 @@ tauri = { version = "2", features = [
|
|
|
34
34
|
tauri-plugin-shell = "2"
|
|
35
35
|
tauri-plugin-global-shortcut = "2"
|
|
36
36
|
tauri-plugin-single-instance = "2"
|
|
37
|
+
# Registers the `clips://` custom URL scheme so a web "Open desktop app"
|
|
38
|
+
# click can launch/focus the running tray app. macOS registration is baked
|
|
39
|
+
# into Info.plist at build time from `plugins.deep-link` in tauri.conf.json;
|
|
40
|
+
# Windows/Linux register at runtime via `register_all()`.
|
|
41
|
+
tauri-plugin-deep-link = "2"
|
|
37
42
|
tauri-plugin-updater = "2"
|
|
38
43
|
tauri-plugin-process = "2"
|
|
39
44
|
tauri-plugin-autostart = "2.5.1"
|