@agent-native/core 0.106.0 → 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 +12 -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/application-state/index.ts +2 -0
- package/corpus/core/src/application-state/script-helpers.ts +12 -0
- package/corpus/core/src/application-state/store.ts +38 -0
- package/corpus/core/src/client/review/ReviewCommentComposer.tsx +10 -3
- 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/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +22 -0
- package/corpus/templates/design/AGENTS.md +16 -0
- package/corpus/templates/design/actions/cancel-node-rewrite-request.ts +54 -0
- package/corpus/templates/design/actions/propose-node-rewrite.ts +290 -0
- package/corpus/templates/design/actions/resolve-node-rewrite.ts +257 -0
- package/corpus/templates/design/actions/view-screen.ts +98 -2
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +71 -1
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +19 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +33 -0
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +194 -0
- package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +21 -4
- package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +18 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -0
- package/corpus/templates/design/app/components/visual-editor/NodeRewriteProposal.tsx +638 -0
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +535 -54
- package/corpus/templates/design/app/components/visual-editor/index.ts +5 -1
- package/corpus/templates/design/app/components/visual-editor/review-canvas-state.ts +21 -3
- package/corpus/templates/design/app/i18n/ar-SA.ts +35 -0
- package/corpus/templates/design/app/i18n/de-DE.ts +35 -0
- package/corpus/templates/design/app/i18n/es-ES.ts +35 -0
- package/corpus/templates/design/app/i18n/fr-FR.ts +35 -0
- package/corpus/templates/design/app/i18n/hi-IN.ts +35 -0
- package/corpus/templates/design/app/i18n/ja-JP.ts +35 -0
- package/corpus/templates/design/app/i18n/ko-KR.ts +35 -0
- package/corpus/templates/design/app/i18n/pt-BR.ts +35 -0
- package/corpus/templates/design/app/i18n/zh-CN.ts +35 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +32 -0
- package/corpus/templates/design/app/i18n-data.ts +32 -0
- package/corpus/templates/design/app/lib/node-reprompt.ts +110 -0
- package/corpus/templates/design/changelog/2026-07-16-design-editing-and-review-are-now-more-predictable.md +6 -0
- package/corpus/templates/design/server/lib/reprompt-action-guard.ts +107 -0
- package/corpus/templates/design/server/plugins/agent-chat.ts +9 -1
- package/corpus/templates/design/shared/code-layer.ts +15 -0
- package/corpus/templates/design/shared/node-rewrite.ts +213 -0
- package/corpus/templates/design/shared/review-anchor.ts +26 -3
- 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/application-state/index.d.ts +2 -2
- package/dist/application-state/index.d.ts.map +1 -1
- package/dist/application-state/index.js +2 -2
- package/dist/application-state/index.js.map +1 -1
- package/dist/application-state/script-helpers.d.ts +1 -0
- package/dist/application-state/script-helpers.d.ts.map +1 -1
- package/dist/application-state/script-helpers.js +7 -1
- package/dist/application-state/script-helpers.js.map +1 -1
- package/dist/application-state/store.d.ts +1 -0
- package/dist/application-state/store.d.ts.map +1 -1
- package/dist/application-state/store.js +28 -0
- package/dist/application-state/store.js.map +1 -1
- package/dist/client/review/ReviewCommentComposer.d.ts +4 -1
- package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
- package/dist/client/review/ReviewCommentComposer.js +3 -3
- package/dist/client/review/ReviewCommentComposer.js.map +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/notifications/routes.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- 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/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.106.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7fb8e89: Return a verified artifact receipt when an integration mutation succeeds without a final model summary.
|
|
8
|
+
|
|
9
|
+
## 0.106.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 0d8118e: Allow review comment composers to render a custom agent action in the standard action row, choose the Enter-key submit target, and atomically compare-and-set application state for race-safe UI workflows.
|
|
14
|
+
|
|
3
15
|
## 0.106.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
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");
|
|
@@ -4,6 +4,7 @@ export {
|
|
|
4
4
|
appStateGetMany,
|
|
5
5
|
appStatePut,
|
|
6
6
|
appStateDelete,
|
|
7
|
+
appStateCompareAndSet,
|
|
7
8
|
appStateList,
|
|
8
9
|
appStateDeleteByPrefix,
|
|
9
10
|
} from "./store.js";
|
|
@@ -33,6 +34,7 @@ export {
|
|
|
33
34
|
readAppState,
|
|
34
35
|
writeAppState,
|
|
35
36
|
deleteAppState,
|
|
37
|
+
compareAndSetAppState,
|
|
36
38
|
listAppState,
|
|
37
39
|
deleteAppStateByPrefix,
|
|
38
40
|
readAppStateForCurrentTab,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
appStateGet,
|
|
18
18
|
appStatePut,
|
|
19
19
|
appStateDelete,
|
|
20
|
+
appStateCompareAndSet,
|
|
20
21
|
appStateList,
|
|
21
22
|
appStateDeleteByPrefix,
|
|
22
23
|
} from "./store.js";
|
|
@@ -71,6 +72,17 @@ export async function deleteAppState(key: string): Promise<boolean> {
|
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
export async function compareAndSetAppState(
|
|
76
|
+
key: string,
|
|
77
|
+
expectedValue: Record<string, unknown>,
|
|
78
|
+
nextValue: Record<string, unknown> | null,
|
|
79
|
+
): Promise<boolean> {
|
|
80
|
+
const sessionId = await resolveSessionId();
|
|
81
|
+
return appStateCompareAndSet(sessionId, key, expectedValue, nextValue, {
|
|
82
|
+
requestSource: "agent",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
74
86
|
export async function listAppState(
|
|
75
87
|
prefix: string,
|
|
76
88
|
): Promise<Array<{ key: string; value: Record<string, unknown> }>> {
|
|
@@ -191,6 +191,44 @@ export async function appStateDelete(
|
|
|
191
191
|
return deleted;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
export async function appStateCompareAndSet(
|
|
195
|
+
sessionId: string,
|
|
196
|
+
key: string,
|
|
197
|
+
expectedValue: Record<string, unknown>,
|
|
198
|
+
nextValue: Record<string, unknown> | null,
|
|
199
|
+
options?: StoreWriteOptions,
|
|
200
|
+
): Promise<boolean> {
|
|
201
|
+
await ensureTable();
|
|
202
|
+
const client = getDbExec();
|
|
203
|
+
const expected = JSON.stringify(expectedValue);
|
|
204
|
+
if (nextValue === null) {
|
|
205
|
+
const result = await client.execute({
|
|
206
|
+
sql: `DELETE FROM application_state WHERE session_id = ? AND key = ? AND value = ?`,
|
|
207
|
+
args: [sessionId, key, expected],
|
|
208
|
+
});
|
|
209
|
+
const changed = result.rowsAffected > 0;
|
|
210
|
+
if (changed) emitAppStateDelete(key, options?.requestSource, sessionId);
|
|
211
|
+
return changed;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const next = JSON.stringify(nextValue);
|
|
215
|
+
if (
|
|
216
|
+
!isLocalDatabase() &&
|
|
217
|
+
Buffer.byteLength(next, "utf8") > MAX_HOSTED_APP_STATE_VALUE_BYTES
|
|
218
|
+
) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`application_state value "${key}" is too large for hosted SQL storage. Store large files, base64, or blobs in file storage and write only a URL or handle.`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
const result = await client.execute({
|
|
224
|
+
sql: `UPDATE application_state SET value = ?, updated_at = ? WHERE session_id = ? AND key = ? AND value = ?`,
|
|
225
|
+
args: [next, Date.now(), sessionId, key, expected],
|
|
226
|
+
});
|
|
227
|
+
const changed = result.rowsAffected > 0;
|
|
228
|
+
if (changed) emitAppStateChange(key, options?.requestSource, sessionId);
|
|
229
|
+
return changed;
|
|
230
|
+
}
|
|
231
|
+
|
|
194
232
|
export async function appStateList(
|
|
195
233
|
sessionId: string,
|
|
196
234
|
keyPrefix: string,
|
|
@@ -2,6 +2,7 @@ import { Button } from "@agent-native/toolkit/ui/button";
|
|
|
2
2
|
import { Spinner } from "@agent-native/toolkit/ui/spinner";
|
|
3
3
|
import { Textarea } from "@agent-native/toolkit/ui/textarea";
|
|
4
4
|
import { IconFocus2, IconMessageCircle, IconSend } from "@tabler/icons-react";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
5
6
|
|
|
6
7
|
import type { ReviewResolutionTarget } from "../../review/types.js";
|
|
7
8
|
import { cn } from "../utils.js";
|
|
@@ -13,12 +14,14 @@ export interface ReviewCommentComposerProps {
|
|
|
13
14
|
submittingTarget?: ReviewResolutionTarget | null;
|
|
14
15
|
disabled?: boolean;
|
|
15
16
|
showAgentAction?: boolean;
|
|
17
|
+
agentAction?: ReactNode;
|
|
16
18
|
placeholder?: string;
|
|
17
19
|
commentLabel?: string;
|
|
18
20
|
agentLabel?: string;
|
|
19
21
|
contextLabel?: string;
|
|
20
22
|
autoFocus?: boolean;
|
|
21
23
|
submitOnEnter?: boolean;
|
|
24
|
+
enterSubmitTarget?: ReviewResolutionTarget;
|
|
22
25
|
onEscape?: () => void;
|
|
23
26
|
className?: string;
|
|
24
27
|
}
|
|
@@ -30,12 +33,14 @@ export function ReviewCommentComposer({
|
|
|
30
33
|
submittingTarget = null,
|
|
31
34
|
disabled = false,
|
|
32
35
|
showAgentAction = false,
|
|
36
|
+
agentAction,
|
|
33
37
|
placeholder = "Add a comment...",
|
|
34
38
|
commentLabel = "Comment",
|
|
35
39
|
agentLabel = "Send to agent",
|
|
36
40
|
contextLabel,
|
|
37
41
|
autoFocus = false,
|
|
38
42
|
submitOnEnter = false,
|
|
43
|
+
enterSubmitTarget = "human",
|
|
39
44
|
onEscape,
|
|
40
45
|
className,
|
|
41
46
|
}: ReviewCommentComposerProps) {
|
|
@@ -75,7 +80,7 @@ export function ReviewCommentComposer({
|
|
|
75
80
|
}
|
|
76
81
|
if (submitOnEnter && event.key === "Enter" && !event.shiftKey) {
|
|
77
82
|
event.preventDefault();
|
|
78
|
-
submit(
|
|
83
|
+
submit(enterSubmitTarget);
|
|
79
84
|
}
|
|
80
85
|
}}
|
|
81
86
|
/>
|
|
@@ -84,7 +89,7 @@ export function ReviewCommentComposer({
|
|
|
84
89
|
type="submit"
|
|
85
90
|
size="sm"
|
|
86
91
|
disabled={!canSubmit}
|
|
87
|
-
className="h-8 w-full
|
|
92
|
+
className="h-8 w-full gap-1.5 @2xs/review:w-auto @2xs/review:min-w-28 @2xs/review:shrink-0"
|
|
88
93
|
>
|
|
89
94
|
{submittingTarget === "human" ? (
|
|
90
95
|
<Spinner className="size-3.5" />
|
|
@@ -93,7 +98,9 @@ export function ReviewCommentComposer({
|
|
|
93
98
|
)}
|
|
94
99
|
<span className="truncate">{commentLabel}</span>
|
|
95
100
|
</Button>
|
|
96
|
-
{showAgentAction ? (
|
|
101
|
+
{showAgentAction && agentAction ? (
|
|
102
|
+
agentAction
|
|
103
|
+
) : showAgentAction ? (
|
|
97
104
|
<Button
|
|
98
105
|
type="button"
|
|
99
106
|
size="sm"
|
|
@@ -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é)",
|