@@ -26,6 +26,9 @@ export enum ConversationIntentType {
2626 OpenFilePreview = 'open_file_preview',
2727 DownloadFile = 'download_file',
2828 BuildPlan = 'build_plan',
29+ RollbackSession = 'rollback_session',
30+ RollbackResync = 'rollback_resync',
31+ RollbackNotice = 'rollback_notice',
2932 Send = 'send',
3033 VoiceInput = 'voice_input',
3134 ChatInputChanged = 'chat_input_changed'
@@ -41,6 +44,26 @@ export class PlanBuildRequest {
4144 }
4245}
4346
47+ /**
48+ * One staged rollback the view asks the host for.
49+ *
50+ * `sendText` is what makes the two exits one command: an empty value is the plain
51+ * confirm, which leaves the composer alone, while a non-empty value is the
52+ * send-while-staging exit, where the rollback has to complete before that text is
53+ * sent from the withdrawn point. Both travel together because the order between
54+ * them is the whole contract and splitting them into two intents would let a
55+ * caller reverse it.
56+ */
57+ export class SessionRollbackRequest {
58+ readonly targetTurnId: string;
59+ readonly sendText: string;
60+
61+ constructor(targetTurnId: string, sendText: string = '') {
62+ this.targetTurnId = targetTurnId;
63+ this.sendText = sendText;
64+ }
65+ }
66+
4467/**
4568 * What a `CopyMessage` intent asks the clipboard owner to write.
4669 *
@@ -65,6 +88,10 @@ export class ConversationIntent {
6588 readonly answers?: ConversationUiQuestionAnswer;
6689 readonly filePreviewRequest?: FilePreviewRequest;
6790 readonly planBuildRequest?: PlanBuildRequest;
91+ // Assigned by the factory rather than through the constructor, the same way
92+ // `mailboxAction` is: the class's positional constructor is already long, and a
93+ // payload only one intent type reads does not belong in every other one's call.
94+ rollbackRequest?: SessionRollbackRequest;
6895 readonly copyScope: MessageCopyScope;
6996 mailboxAction?: InteractionMailboxAction;
7097
@@ -154,4 +181,30 @@ export class ConversationIntents {
154181 new PlanBuildRequest(path, name)
155182 );
156183 }
184+
185+ /**
186+ * The one rollback intent, for both staged exits.
187+ *
188+ * `sendText` is empty for a confirm and non-empty for send-while-staging; the
189+ * host command itself is identical, and the viewmodel decides whether a message
190+ * follows it.
191+ */
192+ static rollback(targetTurnId: string, sendText: string = ''): ConversationIntent {
193+ const intent = new ConversationIntent(ConversationIntentType.RollbackSession);
194+ intent.rollbackRequest = new SessionRollbackRequest(targetTurnId, sendText);
195+ return intent;
196+ }
197+
198+ /** Refetch the authoritative transcript after a host outcome this client refused. */
199+ static rollbackResync(): ConversationIntent {
200+ return new ConversationIntent(ConversationIntentType.RollbackResync);
201+ }
202+
203+ /**
204+ * Say one refusal out loud. The value is a dotted i18n key, not a sentence: the
205+ * policy owns which key a given denial resolves to and this app owns no copy.
206+ */
207+ static rollbackNotice(key: string): ConversationIntent {
208+ return new ConversationIntent(ConversationIntentType.RollbackNotice, key);
209+ }
157210}
0 commit comments