Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bf58c80
feat: introduce optional integration discovery during setup
roomote Sep 9, 2026
d4c2f51
improve: suggest only named tools during setup
roomote Sep 9, 2026
9494801
feat: make guided interactions reliable
roomote Sep 10, 2026
7c267b1
fix: keep guided interaction responses reliable
roomote Sep 10, 2026
ccf3b2a
fix: recover setup preset continuation
roomote Sep 10, 2026
b35806b
fix: continue setup after guided responses
roomote Sep 10, 2026
8d709ce
fix: resume zero-match setup discovery
roomote Sep 10, 2026
a65534d
fix: preserve guided interaction ownership
roomote Sep 10, 2026
58e58b9
fix: restore collaboration after setup
roomote Sep 10, 2026
3c93484
fix: resolve setup cards after completion
roomote Sep 10, 2026
40b555b
fix: continue completed setup card actions
roomote Sep 11, 2026
b61c770
fix: tolerate placeholder setup input fields
brunobergher Sep 11, 2026
5335dae
fix: avoid duplicate starter selection confirmation
brunobergher Sep 11, 2026
db65820
Fix setup timeline child key warning
brunobergher Sep 11, 2026
357a875
fix transcript interaction receipt deduplication
brunobergher Sep 11, 2026
ef35b08
feat setup receipts as transcript activities
brunobergher Sep 11, 2026
a775302
chore: resolve rebase lint issues
brunobergher Sep 11, 2026
64ea287
fix: use effective integrations for voice status
brunobergher Sep 11, 2026
e73e244
feat: personalize guided setup opening
brunobergher Sep 11, 2026
37a50d3
fix: preserve hidden voice delivery rows
roomote Sep 11, 2026
93a6ab5
fix: remove setup profile questions
roomote Sep 11, 2026
70e2da5
test: keep declarative environment fixture invalid
roomote Sep 11, 2026
5340f39
test: isolate provider usage responses
roomote Sep 11, 2026
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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

221 changes: 221 additions & 0 deletions apps/api/src/handlers/discord/__tests__/request-user-input.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

88 changes: 84 additions & 4 deletions apps/api/src/handlers/discord/request-user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@ import {
buildDiscordCancelledRequestUserInputText,
getDiscordRequestUserInputCurrentQuestion,
getPendingCommunicationRequestUserInput,
matchesDiscordRequestUserInputRequestToken,
parseDiscordRequestUserInputAnswerCallbackData,
parseDiscordRequestUserInputCancelCallbackData,
rebindPendingCommunicationRequestUserInputRun,
submitPendingCommunicationRequestUserInputAnswer,
type PendingCommunicationRequestUserInput,
} from '@roomote/communication';
import type { DiscordInteraction } from '@roomote/communication/discord-event';
import type { DiscordCommunicationProvider } from '@roomote/communication/discord-provider';
import { type AcpRequestUserInputAnswers } from '@roomote/types';
import {
TaskPayloadKind,
type AcpRequestUserInputAnswers,
} from '@roomote/types';
import { setTrustedRunActingUserOnSuccess } from '@roomote/db/server';
import { findActiveCommunicationTaskRun } from '@roomote/sdk/server/communication';

import { apiLogger } from '../../logging.js';
import { replyToDiscordEvent } from './replies.js';
Expand Down Expand Up @@ -186,7 +192,7 @@ export async function tryHandleDiscordRequestUserInputCallback(params: {
}

const conversationId = conversationIdForChannel(params.channel);
const pendingRequest = await getPendingCommunicationRequestUserInput(
let pendingRequest = await getPendingCommunicationRequestUserInput(
'discord',
conversationId,
);
Expand All @@ -207,10 +213,15 @@ export async function tryHandleDiscordRequestUserInputCallback(params: {
return true;
}

const expectedToken = pendingRequest.requestId.slice(-8);
const receivedToken =
answerCallback?.requestToken ?? cancelCallback?.requestToken;
if (receivedToken !== expectedToken) {
if (
!receivedToken ||
!matchesDiscordRequestUserInputRequestToken(
pendingRequest.requestId,
receivedToken,
)
) {
await replyToDiscordEvent({
provider: params.provider,
applicationId: params.applicationId,
Expand All @@ -225,6 +236,75 @@ export async function tryHandleDiscordRequestUserInputCallback(params: {
return true;
}

const activeRun = await findActiveCommunicationTaskRun({
provider: 'discord',
channelId: params.channel.parentChannelId ?? params.channel.channelId,
...(params.channel.parentChannelId
? { threadId: params.channel.channelId }
: {}),
taskId: pendingRequest.taskId,
});
if (!activeRun) {
await replyToDiscordEvent({
provider: params.provider,
applicationId: params.applicationId,
channel: params.channel,
interaction: {
interaction: params.interaction,
interactionDeferred: params.interactionDeferred,
},
text: 'This prompt is no longer active.',
ephemeral: true,
});
return true;
}

if (activeRun.id !== pendingRequest.runId) {
const sourceRunId =
activeRun.payloadKind === TaskPayloadKind.SnapshotResume &&
activeRun.payload &&
typeof activeRun.payload === 'object'
? (activeRun.payload as { sourceRunId?: unknown }).sourceRunId
: undefined;
if (sourceRunId !== pendingRequest.runId) {
await replyToDiscordEvent({
provider: params.provider,
applicationId: params.applicationId,
channel: params.channel,
interaction: {
interaction: params.interaction,
interactionDeferred: params.interactionDeferred,
},
text: 'This prompt is no longer active.',
ephemeral: true,
});
return true;
}

const rebound = await rebindPendingCommunicationRequestUserInputRun({
provider: 'discord',
conversationId,
taskId: pendingRequest.taskId,
sourceRunId: pendingRequest.runId,
resumedRunId: activeRun.id,
});
if (!rebound) {
await replyToDiscordEvent({
provider: params.provider,
applicationId: params.applicationId,
channel: params.channel,
interaction: {
interaction: params.interaction,
interactionDeferred: params.interactionDeferred,
},
text: 'This prompt is no longer active.',
ephemeral: true,
});
return true;
}
pendingRequest = { ...pendingRequest, runId: activeRun.id };
}

if (pendingRequest.status === 'submitted') {
await postAlreadyReceivedNotice({
provider: params.provider,
Expand Down
13 changes: 13 additions & 0 deletions apps/docs/self-hosting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,19 @@ recommendations. Detailed provider instructions and credential entry open in a
dialog from the source-control card; Roomote never asks for credentials in
chat.

The conversation also asks briefly about the tools your team uses for
documents, monitoring, and project tracking, one topic at a time.
You can skip these questions. The optional integrations card lists only supported
tools you said you use and opens their secure configuration without leaving setup.
If there are no eligible matches, setup moves on without showing suggestions.
Tools without a built-in connector are not presented as supported. Use
**Keep going** to move on without connecting; you can connect tools later in
Settings. Integration choices do not change the starter tasks offered.
Services that are also source-control, communications, inference, or sandbox
providers are excluded from this optional step; their separate setup is unchanged.
The Vercel deployments integration remains available separately from Vercel AI
Gateway inference.

Setup completes once inference and a sandbox provider are ready, source control
is successfully configured, and at least one repository has synchronized.
Roomote then offers preselected starter tasks in one structured multi-select
Expand Down
Loading
Loading