feat(v2/approvals): per-card titles and structured options

Approval cards now carry a required title (Add MCP Request, Install
Packages Request, Rebuild Request, Credentials Request) and structured
options with distinct pre-click label, post-click selectedLabel (e.g.
" Approved" / " Rejected"), and value used for click routing. The
title and normalized options are persisted in pending_questions so the
post-click card edit can render the correct per-type title and selected
label on both chat-sdk channels and Discord interactions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Koshkoshinsk
2026-04-14 14:49:01 +00:00
parent 8d60af71d3
commit d92d75e173
15 changed files with 211 additions and 51 deletions

View File

@@ -375,11 +375,15 @@ describe('pending questions', () => {
platform_id: 'chan-1',
channel_type: 'discord',
thread_id: null,
title: 'Test',
options: [{ label: 'Yes', selectedLabel: 'Yes', value: 'yes' }],
created_at: now(),
});
const result = getPendingQuestion('q-1');
expect(result).toBeDefined();
expect(result!.session_id).toBe('sess-1');
expect(result!.title).toBe('Test');
expect(result!.options[0].value).toBe('yes');
});
it('should delete', () => {
@@ -390,6 +394,8 @@ describe('pending questions', () => {
platform_id: null,
channel_type: null,
thread_id: null,
title: 'Test',
options: [{ label: 'Yes', selectedLabel: 'Yes', value: 'yes' }],
created_at: now(),
});
deletePendingQuestion('q-1');

View File

@@ -61,6 +61,8 @@ export const migration001: Migration = {
platform_id TEXT,
channel_type TEXT,
thread_id TEXT,
title TEXT NOT NULL,
options_json TEXT NOT NULL,
created_at TEXT NOT NULL
);
`);

View File

@@ -64,6 +64,8 @@ CREATE TABLE pending_questions (
platform_id TEXT,
channel_type TEXT,
thread_id TEXT,
title TEXT NOT NULL,
options_json TEXT NOT NULL,
created_at TEXT NOT NULL
);
`;

View File

@@ -75,16 +75,29 @@ export function deleteSession(id: string): void {
export function createPendingQuestion(pq: PendingQuestion): void {
getDb()
.prepare(
`INSERT INTO pending_questions (question_id, session_id, message_out_id, platform_id, channel_type, thread_id, created_at)
VALUES (@question_id, @session_id, @message_out_id, @platform_id, @channel_type, @thread_id, @created_at)`,
`INSERT INTO pending_questions (question_id, session_id, message_out_id, platform_id, channel_type, thread_id, title, options_json, created_at)
VALUES (@question_id, @session_id, @message_out_id, @platform_id, @channel_type, @thread_id, @title, @options_json, @created_at)`,
)
.run(pq);
.run({
question_id: pq.question_id,
session_id: pq.session_id,
message_out_id: pq.message_out_id,
platform_id: pq.platform_id,
channel_type: pq.channel_type,
thread_id: pq.thread_id,
title: pq.title,
options_json: JSON.stringify(pq.options),
created_at: pq.created_at,
});
}
export function getPendingQuestion(questionId: string): PendingQuestion | undefined {
return getDb().prepare('SELECT * FROM pending_questions WHERE question_id = ?').get(questionId) as
| PendingQuestion
| undefined;
const row = getDb()
.prepare('SELECT * FROM pending_questions WHERE question_id = ?')
.get(questionId) as (Omit<PendingQuestion, 'options'> & { options_json: string }) | undefined;
if (!row) return undefined;
const { options_json, ...rest } = row;
return { ...rest, options: JSON.parse(options_json) };
}
export function deletePendingQuestion(questionId: string): void {