Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion packages/openclaw/src/monitor/approval.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { A2UI } from '@tloncorp/api';
import { describe, expect, it } from 'vitest';

import { A2UI } from '../urbit/a2ui.js';
import {
APPROVAL_TTL_MS,
type DisplayContext,
Expand Down Expand Up @@ -244,6 +244,67 @@ describe('buildApprovalA2UIBlob', () => {
}
});

it('adds view message navigation for dm and channel approvals with source messages', () => {
const dm = buildApprovalA2UIBlob({
id: 'da1b2',
type: 'dm',
requestingShip: '~sampel-palnet',
timestamp: 1,
messagePreview: 'Hello, I would like to chat with your bot.',
originalMessage: {
messageId: '170.141.184.507',
messageText: 'Hello, I would like to chat with your bot.',
messageContent: [],
timestamp: 1,
},
});
const channel = buildApprovalA2UIBlob(
{
id: 'c3d4e',
type: 'channel',
requestingShip: '~littel-wolfur',
channelNest: 'chat/~host/general',
timestamp: 1,
messagePreview: '@bot can you review this build before I merge?',
originalMessage: {
messageId: '170.141.184.621',
messageText: '@bot can you review this build before I merge?',
messageContent: [],
timestamp: 1,
parentId: '170.141.184.600',
},
},
ctx
);

expect(A2UI.validateBlobEntry(dm)).toBe(true);
expect(A2UI.validateBlobEntry(channel)).toBe(true);
expect(JSON.stringify(dm)).toContain('View message');
expect(JSON.stringify(dm)).toContain('"name":"tlon.navigate"');
expect(JSON.stringify(dm)).toContain('"channelId":"~sampel-palnet"');
expect(JSON.stringify(dm)).toContain('"postId":"170.141.184.507"');
expect(JSON.stringify(channel)).toContain(
'"channelId":"chat/~host/general"'
);
expect(JSON.stringify(channel)).toContain('"parentId":"170.141.184.600"');
expect(JSON.stringify(channel)).toContain('"groupId":"~host/cool-group"');
});

it('does not add view message navigation to group invites', () => {
const approval = buildApprovalA2UIBlob({
id: 'g5f6a',
type: 'group',
requestingShip: '~robin-dasler',
groupFlag: '~robin-dasler/garden-club',
groupTitle: 'Garden Club',
timestamp: 1,
});

expect(A2UI.validateBlobEntry(approval)).toBe(true);
expect(JSON.stringify(approval)).not.toContain('View message');
expect(JSON.stringify(approval)).not.toContain('tlon.navigate');
});

it('formats the visible notification text by request type', () => {
expect(
formatApprovalRequestNotification(
Expand Down
67 changes: 65 additions & 2 deletions packages/openclaw/src/monitor/approval.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { A2UI } from '@tloncorp/api';
import { randomUUID } from 'node:crypto';

/**
Expand All @@ -9,6 +8,7 @@ import { randomUUID } from 'node:crypto';
* (/allow, /reject, /ban).
*/
import { APPROVAL_TTL_MS, type PendingApproval } from '../settings.js';
import { A2UI } from '../urbit/a2ui.js';
import { type TlonA2UIBlob, makeA2UIBlob } from '../urbit/blob.js';

export type { PendingApproval };
Expand Down Expand Up @@ -225,6 +225,7 @@ type ApprovalA2UIParams = {
groupName?: string;
groupFlag?: string;
groupTitle?: string;
sourceTarget?: A2UI.NavigationTarget;
};

function approvalRequesterName(params: ApprovalA2UIParams): string {
Expand Down Expand Up @@ -320,12 +321,14 @@ function buildApprovalA2UIBlobFromParams(
const contextLines = approvalContextLines(params);
const contextIds = contextLines.map((_, index) => `context${index}`);
const copy = approvalCopy(params);
const actionChildren = ['allow', 'reject', 'ban'];
const bodyChildren = [
'eyebrow',
'title',
'titleDivider',
...contextIds,
...(copy ? ['copy'] : []),
...(params.sourceTarget ? ['sourceAction'] : []),
'divider',
'details',
'actions',
Expand All @@ -348,6 +351,9 @@ function buildApprovalA2UIBlobFromParams(
},
]
: [];
const sourceActionComponents: A2UI.Component[] = params.sourceTarget
? [{ id: 'sourceAction', component: 'Row', children: ['viewMessage'] }]
: [];

const components: A2UI.Component[] = [
{ id: 'root', component: 'Card', child: 'body' },
Expand All @@ -371,6 +377,7 @@ function buildApprovalA2UIBlobFromParams(
{ id: 'titleDivider', component: 'Divider' },
...contextComponents,
...copyComponents,
...sourceActionComponents,
{ id: 'divider', component: 'Divider' },
{
id: 'details',
Expand All @@ -386,8 +393,29 @@ function buildApprovalA2UIBlobFromParams(
{
id: 'actions',
component: 'Row',
children: ['allow', 'reject', 'ban'],
children: actionChildren,
},
...(params.sourceTarget
? [
{
id: 'viewMessage',
component: 'Button',
variant: 'secondary',
child: 'viewMessageLabel',
action: {
event: {
name: A2UI.action.navigate,
context: { target: params.sourceTarget },
},
},
} as const,
{
id: 'viewMessageLabel',
component: 'Text',
text: 'View message',
} as const,
]
: []),
{
id: 'allow',
component: 'Button',
Expand Down Expand Up @@ -473,6 +501,40 @@ function displayGroupForApproval(
return titleOverride || ctx?.groupNames?.get(flag) || flag;
}

function approvalSourceTarget(
approval: PendingApproval,
ctx?: DisplayContext
): A2UI.NavigationTarget | undefined {
const messageId = approval.originalMessage?.messageId;
if (!messageId) {
return undefined;
}

const base = {
type: 'message' as const,
postId: messageId,
authorId: approval.requestingShip,
parentId: approval.originalMessage?.parentId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include parent authors for reply source links

When the approval came from a thread reply and the owner taps View message, the app's A2UI handler needs target.parentAuthorId if the parent post is not already in the local DB; otherwise it logs missing parent post author and returns before navigating (see packages/app/hooks/useA2UINavigation.ts:80-90). This target only carries parentId, so reply-source approvals from restricted channels/DMs can render a button that does nothing in that common uncached-parent case.

Useful? React with 👍 / 👎.

};

if (approval.type === 'dm') {
return {
...base,
channelId: approval.requestingShip,
};
}

if (approval.type === 'channel' && approval.channelNest) {
return {
...base,
channelId: approval.channelNest,
groupId: ctx?.channelGroups?.get(approval.channelNest),
};
}

return undefined;
}

export function buildApprovalA2UIBlob(
approval: PendingApproval,
ctx?: DisplayContext
Expand All @@ -494,6 +556,7 @@ export function buildApprovalA2UIBlob(
approval.groupTitle,
ctx
),
sourceTarget: approvalSourceTarget(approval, ctx),
});
}

Expand Down
Loading
Loading