@cat-factory/app 0.304.0 → 0.306.0

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.
@@ -0,0 +1,254 @@
1
+ <script setup lang="ts">
2
+ // One exploration thread: its messages, the comment drafts it produced, and a composer. While its
3
+ // own answer is pending the composer waits; other threads are unaffected.
4
+ import { isPostableDraft } from '@cat-factory/contracts'
5
+ import type { GuidedReviewCommentDraft, GuidedReviewThreadView } from '~/types/domain'
6
+ import MarkdownProse from '~/components/common/MarkdownProse.vue'
7
+ import GuidedReviewFailure from '~/components/guidedReview/GuidedReviewFailure.vue'
8
+ import GuidedReviewDraftCard from '~/components/guidedReview/GuidedReviewDraftCard.vue'
9
+ import {
10
+ canAsk,
11
+ citationLabel,
12
+ droppedAnchor,
13
+ isLive,
14
+ keptDrafts,
15
+ postableSelection,
16
+ } from '~/components/guidedReview/GuidedReview.logic'
17
+
18
+ const props = defineProps<{
19
+ sessionId: string
20
+ /** Absent for the unsaved new-thread tab: its first question creates the thread. */
21
+ view: GuidedReviewThreadView | undefined
22
+ drafts: GuidedReviewCommentDraft[]
23
+ }>()
24
+ const emit = defineEmits<{ created: [threadId: string] }>()
25
+ const { t } = useI18n()
26
+ const store = useGuidedReviewStore()
27
+ const { present } = usePipelineErrorToast()
28
+
29
+ const prompt = ref('')
30
+ const sending = ref(false)
31
+ const messages = computed(() => props.view?.messages ?? [])
32
+ const ready = computed(() => canAsk(messages.value) && !sending.value)
33
+ const canSend = computed(() => ready.value && prompt.value.trim().length > 0)
34
+
35
+ async function send(): Promise<void> {
36
+ if (!canSend.value) return
37
+ const content = prompt.value.trim()
38
+ sending.value = true
39
+ try {
40
+ if (props.view) {
41
+ await store.ask(props.sessionId, props.view.thread.id, { content })
42
+ } else {
43
+ const created = await store.openThread(props.sessionId, { question: { content } })
44
+ emit('created', created.thread.id)
45
+ }
46
+ prompt.value = ''
47
+ } catch (error) {
48
+ present(error, 'guidedReview.errors.ask')
49
+ } finally {
50
+ sending.value = false
51
+ }
52
+ }
53
+
54
+ const selected = ref(new Set<string>())
55
+ const summary = ref('')
56
+ const posting = ref(false)
57
+ const postResult = ref<{
58
+ posted: number
59
+ failed: number
60
+ skipped: number
61
+ summaryError: string | null
62
+ } | null>(null)
63
+ // A `posting` draft becomes postable again once its claim outlives the lease, so the clock only
64
+ // runs while one is on screen.
65
+ const now = useNowTick(30_000, () => props.drafts.some((d) => d.status === 'posting'))
66
+ const toPost = computed(() => postableSelection(props.drafts, selected.value, now.value))
67
+
68
+ function setSelected(id: string, value: boolean): void {
69
+ const next = new Set(selected.value)
70
+ if (value) next.add(id)
71
+ else next.delete(id)
72
+ selected.value = next
73
+ }
74
+
75
+ async function post(): Promise<void> {
76
+ if (!toPost.value.length) return
77
+ posting.value = true
78
+ try {
79
+ const result = await store.postDrafts(props.sessionId, toPost.value, summary.value)
80
+ const summaryFailed = result.summary.posted === false
81
+ postResult.value = {
82
+ posted: result.posted,
83
+ failed: result.failed,
84
+ skipped: result.skipped.length,
85
+ summaryError: summaryFailed ? (result.summary.error ?? '') : null,
86
+ }
87
+ selected.value = new Set()
88
+ // A summary the host refused stays in the box, so the reviewer can post it with a retry.
89
+ if (!summaryFailed) summary.value = ''
90
+ } catch (error) {
91
+ present(error, 'guidedReview.errors.postDrafts')
92
+ } finally {
93
+ posting.value = false
94
+ }
95
+ }
96
+
97
+ async function draftComments(): Promise<void> {
98
+ if (!props.view || !ready.value) return
99
+ sending.value = true
100
+ try {
101
+ await store.requestDrafts(props.sessionId, props.view.thread.id)
102
+ } catch (error) {
103
+ present(error, 'guidedReview.errors.drafts')
104
+ } finally {
105
+ sending.value = false
106
+ }
107
+ }
108
+ </script>
109
+
110
+ <template>
111
+ <div class="flex h-full min-h-0 flex-col gap-3" data-testid="guided-review-thread">
112
+ <div class="min-h-0 flex-1 space-y-3 overflow-y-auto pr-1">
113
+ <p v-if="!messages.length" class="text-sm text-muted">
114
+ {{ t('guidedReview.thread.empty') }}
115
+ </p>
116
+ <div
117
+ v-for="m in messages"
118
+ :key="m.id"
119
+ :class="m.role === 'user' ? 'ms-8 bg-elevated' : 'me-8 border border-default'"
120
+ class="rounded-lg p-3 text-sm"
121
+ :data-testid="`guided-review-message-${m.role}`"
122
+ >
123
+ <template v-if="m.role === 'user'">
124
+ <p v-if="m.kind === 'comment-drafts'" class="text-xs text-dimmed">
125
+ {{ t('guidedReview.thread.draftRequest') }}
126
+ </p>
127
+ <p class="whitespace-pre-wrap">{{ m.content }}</p>
128
+ </template>
129
+ <p v-else-if="isLive(m)" class="flex items-center gap-2 text-muted">
130
+ <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
131
+ {{ t('guidedReview.thread.waiting') }}
132
+ </p>
133
+ <GuidedReviewFailure v-else-if="m.status === 'failed' && m.failure" :failure="m.failure" />
134
+ <template v-else-if="m.kind === 'answer'">
135
+ <MarkdownProse :text="m.content" />
136
+ <div v-if="m.citations.length" class="mt-2 flex flex-wrap gap-1">
137
+ <UBadge
138
+ v-for="(c, i) in m.citations"
139
+ :key="i"
140
+ size="sm"
141
+ variant="outline"
142
+ color="neutral"
143
+ >
144
+ {{ citationLabel(c) }}
145
+ </UBadge>
146
+ </div>
147
+ </template>
148
+ <template v-else>
149
+ <p>
150
+ {{
151
+ t('guidedReview.drafts.summary', {
152
+ kept: keptDrafts(drafts, m.id),
153
+ proposed: m.draftReport?.proposed ?? 0,
154
+ })
155
+ }}
156
+ </p>
157
+ <ul v-if="m.draftReport?.dropped.length" class="mt-1 space-y-0.5 text-xs text-muted">
158
+ <li
159
+ v-for="(d, i) in m.draftReport.dropped"
160
+ :key="i"
161
+ data-testid="guided-review-dropped"
162
+ >
163
+ {{ droppedAnchor(d) }}:
164
+ {{ t(`guidedReview.dropped.${d.reason}`) }}
165
+ </li>
166
+ </ul>
167
+ </template>
168
+ </div>
169
+
170
+ <div v-if="drafts.length" class="space-y-2" data-testid="guided-review-drafts">
171
+ <h4 class="text-xs font-semibold uppercase text-dimmed">
172
+ {{ t('guidedReview.drafts.title') }}
173
+ </h4>
174
+ <GuidedReviewDraftCard
175
+ v-for="d in drafts"
176
+ :key="d.id"
177
+ :session-id="sessionId"
178
+ :draft="d"
179
+ :selected="selected.has(d.id)"
180
+ :postable="isPostableDraft(d, now)"
181
+ @update:selected="setSelected(d.id, $event)"
182
+ />
183
+ <div class="space-y-2 rounded-md bg-elevated p-2" data-testid="guided-review-post">
184
+ <UTextarea
185
+ v-model="summary"
186
+ :rows="2"
187
+ autoresize
188
+ class="w-full"
189
+ :placeholder="t('guidedReview.drafts.summaryPlaceholder')"
190
+ />
191
+ <div class="flex flex-wrap items-center gap-2">
192
+ <UButton
193
+ color="primary"
194
+ icon="i-lucide-upload"
195
+ :loading="posting"
196
+ :disabled="!toPost.length"
197
+ data-testid="guided-review-post-drafts"
198
+ @click="post"
199
+ >
200
+ {{ t('guidedReview.drafts.post', { count: toPost.length }) }}
201
+ </UButton>
202
+ <p v-if="postResult" class="text-xs text-muted" data-testid="guided-review-post-result">
203
+ {{ t('guidedReview.drafts.postResult', postResult) }}
204
+ </p>
205
+ <p
206
+ v-if="postResult && postResult.summaryError !== null"
207
+ class="text-xs text-error"
208
+ data-testid="guided-review-post-summary-error"
209
+ >
210
+ {{ t('guidedReview.drafts.summaryFailed') }}: {{ postResult.summaryError }}
211
+ </p>
212
+ </div>
213
+ </div>
214
+ </div>
215
+ </div>
216
+
217
+ <div class="space-y-2">
218
+ <UTextarea
219
+ v-model="prompt"
220
+ :rows="3"
221
+ autoresize
222
+ :maxrows="10"
223
+ class="w-full"
224
+ :disabled="!ready"
225
+ :placeholder="ready ? t('guidedReview.thread.placeholder') : t('guidedReview.thread.busy')"
226
+ data-testid="guided-review-composer"
227
+ @keydown.enter.meta.prevent="send"
228
+ @keydown.enter.ctrl.prevent="send"
229
+ />
230
+ <div class="flex flex-wrap items-center gap-2">
231
+ <UButton
232
+ color="primary"
233
+ icon="i-lucide-send"
234
+ :loading="sending"
235
+ :disabled="!canSend"
236
+ data-testid="guided-review-send"
237
+ @click="send"
238
+ >
239
+ {{ t('guidedReview.thread.send') }}
240
+ </UButton>
241
+ <UButton
242
+ v-if="view"
243
+ variant="soft"
244
+ icon="i-lucide-message-square-plus"
245
+ :disabled="!ready || !messages.length"
246
+ data-testid="guided-review-draft-comments"
247
+ @click="draftComments"
248
+ >
249
+ {{ t('guidedReview.drafts.request') }}
250
+ </UButton>
251
+ </div>
252
+ </div>
253
+ </div>
254
+ </template>
@@ -23,9 +23,13 @@ import {
23
23
  reviewQueueDirty,
24
24
  reviewSkillQueuePatch,
25
25
  } from '~/components/panels/inspector/TaskReviewTarget.logic'
26
+ import { reviewTaskTarget } from '~/components/guidedReview/GuidedReview.logic'
26
27
 
27
28
  const props = defineProps<{ block: Block }>()
28
29
  const board = useBoardStore()
30
+ const github = useGitHubStore()
31
+ const ui = useUiStore()
32
+ const access = useWorkspaceAccess()
29
33
  const { t } = useI18n()
30
34
 
31
35
  const isReview = computed(() => props.block.taskType === 'review')
@@ -33,6 +37,26 @@ const fields = computed(() => props.block.taskTypeFields ?? null)
33
37
  const url = computed(() => fields.value?.prUrl?.trim() || '')
34
38
  const focus = computed(() => fields.value?.reviewFocus?.trim() || '')
35
39
 
40
+ // Opening a guided review is a member-tier write, gated like the sidebar entry that opens one.
41
+ const canGuide = computed(() => isReview.value && access.canWriteBoard.value)
42
+
43
+ // Nothing loads the repository projection on board open, and the guided-review button below
44
+ // resolves the task's repository from it.
45
+ watch(
46
+ canGuide,
47
+ (can) => {
48
+ if (can) void github.ensureLoaded().catch(() => {})
49
+ },
50
+ { immediate: true },
51
+ )
52
+
53
+ /** The PR as a guided review opens it, when the task's service has a linked repository. */
54
+ const guidedTarget = computed(() => {
55
+ if (!canGuide.value) return null
56
+ const frame = board.serviceOf(props.block)
57
+ return reviewTaskTarget(fields.value, frame ? github.repoForBlock(frame.id) : undefined)
58
+ })
59
+
36
60
  /** `#123` when the number is known, else the raw link — never an empty affordance. */
37
61
  const label = computed(() => {
38
62
  const number = fields.value?.prNumber
@@ -107,6 +131,18 @@ function revert() {
107
131
  >
108
132
  {{ label }}
109
133
  </p>
134
+ <UButton
135
+ v-if="guidedTarget"
136
+ color="primary"
137
+ variant="soft"
138
+ size="sm"
139
+ icon="i-lucide-scan-search"
140
+ block
141
+ data-testid="inspector-review-target-guided"
142
+ @click="ui.openGuidedReview({ sessionId: null, target: guidedTarget })"
143
+ >
144
+ {{ t('guidedReview.openFromTask') }}
145
+ </UButton>
110
146
  <p v-if="focus" class="text-xs leading-relaxed text-dimmed">
111
147
  {{ t('inspector.reviewTarget.focus', { focus }) }}
112
148
  </p>
@@ -1,16 +1,20 @@
1
1
  import {
2
2
  askGuidedReviewContract,
3
3
  deleteGuidedReviewContract,
4
+ editGuidedReviewDraftContract,
4
5
  getGuidedReviewContract,
5
6
  getGuidedReviewThreadContract,
6
7
  listGuidedReviewsContract,
7
8
  openGuidedReviewContract,
8
9
  openGuidedReviewThreadContract,
10
+ postGuidedReviewDraftsContract,
9
11
  refreshGuidedReviewContract,
10
12
  requestGuidedReviewDraftsContract,
11
13
  } from '@cat-factory/contracts'
12
14
  import type {
13
15
  AskGuidedReviewInput,
16
+ EditGuidedReviewDraftInput,
17
+ PostGuidedReviewDraftsInput,
14
18
  OpenGuidedReviewInput,
15
19
  OpenGuidedReviewThreadInput,
16
20
  } from '~/types/domain'
@@ -76,5 +80,28 @@ export function guidedReviewApi({ send, ws }: ApiContext) {
76
80
  pathParams: { sessionId, threadId },
77
81
  body: { instructions },
78
82
  }),
83
+
84
+ editGuidedReviewDraft: (
85
+ workspaceId: string,
86
+ sessionId: string,
87
+ draftId: string,
88
+ body: EditGuidedReviewDraftInput,
89
+ ) =>
90
+ send(editGuidedReviewDraftContract, {
91
+ pathPrefix: ws(workspaceId),
92
+ pathParams: { sessionId, draftId },
93
+ body,
94
+ }),
95
+
96
+ postGuidedReviewDrafts: (
97
+ workspaceId: string,
98
+ sessionId: string,
99
+ body: PostGuidedReviewDraftsInput,
100
+ ) =>
101
+ send(postGuidedReviewDraftsContract, {
102
+ pathPrefix: ws(workspaceId),
103
+ pathParams: { sessionId },
104
+ body,
105
+ }),
79
106
  }
80
107
  }
@@ -41,6 +41,7 @@ export function useNavContributions() {
41
41
  // Consumer items bypass this map entirely via their own `run` closure.
42
42
  const actions: Record<NavActionId, () => void> = {
43
43
  assistant: () => ui.openAssistant(),
44
+ guidedReview: () => ui.openGuidedReview(),
44
45
  buildPipeline: () => ui.openBuilder(),
45
46
  addFromRepo: () => ui.openAddService(),
46
47
  bootstrapRepo: () => ui.openBootstrap(),
@@ -175,6 +175,14 @@ const CONFLICT_INFO: Record<Exclude<ConflictReason, BespokeConflictReason>, Conf
175
175
  titleKey: 'errors.conflict.title.thread_busy',
176
176
  descriptionKey: 'errors.conflict.description.thread_busy',
177
177
  },
178
+ draft_conflict: {
179
+ titleKey: 'errors.conflict.title.draft_conflict',
180
+ descriptionKey: 'errors.conflict.description.draft_conflict',
181
+ },
182
+ session_stale: {
183
+ titleKey: 'errors.conflict.title.session_stale',
184
+ descriptionKey: 'errors.conflict.description.session_stale',
185
+ },
178
186
  task_limit_reached: {
179
187
  titleKey: 'errors.conflict.title.task_limit_reached',
180
188
  descriptionKey: 'errors.conflict.description.task_limit_reached',
@@ -197,6 +197,7 @@ export interface NavCommandSpec {
197
197
  */
198
198
  export const NAV_ACTIONS = [
199
199
  'assistant',
200
+ 'guidedReview',
200
201
  'buildPipeline',
201
202
  'addFromRepo',
202
203
  'bootstrapRepo',
@@ -395,6 +396,25 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
395
396
  keywordsKey: 'layout.commandBar.keywords.addFromRepo',
396
397
  },
397
398
  },
399
+ {
400
+ // Guided PR review. Basic tier, because reviewing pull requests is part of the everyday
401
+ // delivery loop; not `intake`, because the intake role brings work in rather than reviewing
402
+ // the code that delivers it.
403
+ id: 'guided-review',
404
+ labelKey: 'nav.guidedReview',
405
+ icon: 'i-lucide-scan-search',
406
+ surfaces: S('sidebar', 'command'),
407
+ gate: (g) => g.githubAvailable && g.canWriteBoard,
408
+ action: 'guidedReview',
409
+ testId: 'nav-guided-review',
410
+ sidebar: { group: 'repositories', order: 15 },
411
+ command: {
412
+ group: 'repositories',
413
+ order: 15,
414
+ labelKey: 'layout.commandBar.cmd.guidedReview',
415
+ keywordsKey: 'layout.commandBar.keywords.guidedReview',
416
+ },
417
+ },
398
418
  {
399
419
  id: 'bootstrap-repo',
400
420
  labelKey: 'nav.bootstrapRepo',
@@ -49,6 +49,9 @@ const TaskSourceConnectModal = defineAsyncView(
49
49
  const TaskImportModal = defineAsyncView(() => import('~/components/tasks/TaskImportModal.vue'))
50
50
  const BugHuntModal = defineAsyncView(() => import('~/components/tasks/BugHuntModal.vue'))
51
51
  const AssistantModal = defineAsyncView(() => import('~/components/assistant/AssistantModal.vue'))
52
+ const GuidedReviewModal = defineAsyncView(
53
+ () => import('~/components/guidedReview/GuidedReviewModal.vue'),
54
+ )
52
55
  const RecurringPipelineModal = defineAsyncView(
53
56
  () => import('~/components/board/RecurringPipelineModal.vue'),
54
57
  )
@@ -503,6 +506,7 @@ watch(
503
506
  <TaskImportModal v-if="ui.taskImport" />
504
507
  <BugHuntModal v-if="ui.bugHunt" />
505
508
  <AssistantModal v-if="ui.assistantOpen" />
509
+ <GuidedReviewModal v-if="ui.guidedReview" />
506
510
  <RecurringPipelineModal v-if="ui.addRecurringFrameId" />
507
511
  <ObservabilityPanel v-if="ui.observabilityInstanceId" />
508
512
  <OperatorDashboardPanel v-if="ui.operatorDashboardOpen" />
@@ -2,6 +2,9 @@ import { defineStore } from 'pinia'
2
2
  import { ref } from 'vue'
3
3
  import type {
4
4
  AskGuidedReviewInput,
5
+ EditGuidedReviewDraftInput,
6
+ GuidedReviewCommentDraft,
7
+ GuidedReviewPostResult,
5
8
  GuidedReviewChange,
6
9
  GuidedReviewSessionView,
7
10
  GuidedReviewThreadView,
@@ -112,6 +115,29 @@ export const useGuidedReviewStore = defineStore('guidedReview', () => {
112
115
  await Promise.all([loadThread(sessionId, threadId), loadSession(sessionId)])
113
116
  }
114
117
 
118
+ async function editDraft(
119
+ sessionId: string,
120
+ draftId: string,
121
+ input: EditGuidedReviewDraftInput,
122
+ ): Promise<GuidedReviewCommentDraft> {
123
+ const draft = await api.editGuidedReviewDraft(workspace.requireId(), sessionId, draftId, input)
124
+ await loadSession(sessionId)
125
+ return draft
126
+ }
127
+
128
+ async function postDrafts(
129
+ sessionId: string,
130
+ draftIds: string[],
131
+ summary?: string,
132
+ ): Promise<GuidedReviewPostResult> {
133
+ const result = await api.postGuidedReviewDrafts(workspace.requireId(), sessionId, {
134
+ draftIds,
135
+ ...(summary?.trim() ? { summary } : {}),
136
+ })
137
+ await loadSession(sessionId)
138
+ return result
139
+ }
140
+
115
141
  function forget(sessionId: string) {
116
142
  delete sessions.value[sessionId]
117
143
  delete refetchFailures.value[sessionId]
@@ -193,6 +219,8 @@ export const useGuidedReviewStore = defineStore('guidedReview', () => {
193
219
  openThread,
194
220
  ask,
195
221
  requestDrafts,
222
+ editDraft,
223
+ postDrafts,
196
224
  applyChange,
197
225
  resync,
198
226
  reset,
@@ -1,5 +1,10 @@
1
1
  import { ref } from 'vue'
2
- import type { DocumentSourceKind, InfraSetupArea, TaskSourceKind } from '~/types/domain'
2
+ import type {
3
+ DocumentSourceKind,
4
+ InfraSetupArea,
5
+ OpenGuidedReviewInput,
6
+ TaskSourceKind,
7
+ } from '~/types/domain'
3
8
  import type {
4
9
  InfrastructureScrollTarget,
5
10
  InfrastructureTab,
@@ -185,6 +190,12 @@ function createMiscModals() {
185
190
  return { builderOpen, decisionContext, openBuilder, openDecision, closeDecision }
186
191
  }
187
192
 
193
+ /** What the guided review window opens on: a session, a pull request, or neither (the picker). */
194
+ export interface GuidedReviewOpen {
195
+ sessionId: string | null
196
+ target: OpenGuidedReviewInput | null
197
+ }
198
+
188
199
  /**
189
200
  * Document- and task-source integration modals (keyed by source), plus the add-task /
190
201
  * add-recurring / create-initiative surfaces. The `open*` connect/import handlers reset the hub
@@ -220,6 +231,9 @@ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
220
231
  // subject (a turn resolves every name it needs from the board itself), so a plain flag says
221
232
  // everything the host needs to know.
222
233
  const assistantOpen = ref(false)
234
+ // Guided PR review: either an existing session to show, or a PR to open one for. Both null
235
+ // opens the PR picker.
236
+ const guidedReview = ref<GuidedReviewOpen | null>(null)
223
237
 
224
238
  // Bug hunt: pick a tracker + one of its boards, rank its open unassigned bugs, adopt one.
225
239
  // `containerId` (a service frame or module) preselects where an adopted bug lands; null →
@@ -305,6 +319,13 @@ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
305
319
  function closeAssistant() {
306
320
  assistantOpen.value = false
307
321
  }
322
+ function openGuidedReview(open: GuidedReviewOpen = { sessionId: null, target: null }) {
323
+ resetHubReturn()
324
+ guidedReview.value = open
325
+ }
326
+ function closeGuidedReview() {
327
+ guidedReview.value = null
328
+ }
308
329
  function openBugHunt(source: TaskSourceKind | null = null, containerId: string | null = null) {
309
330
  resetHubReturn()
310
331
  bugHunt.value = { source, containerId }
@@ -354,6 +375,7 @@ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
354
375
  taskConnect,
355
376
  taskImport,
356
377
  assistantOpen,
378
+ guidedReview,
357
379
  bugHunt,
358
380
  startFromDesign,
359
381
  addTaskContainerId,
@@ -375,6 +397,8 @@ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
375
397
  closeTaskImport,
376
398
  openAssistant,
377
399
  closeAssistant,
400
+ openGuidedReview,
401
+ closeGuidedReview,
378
402
  openBugHunt,
379
403
  closeBugHunt,
380
404
  openStartFromDesign,
@@ -2,8 +2,13 @@
2
2
  // these through `~/types/domain` like every other domain type.
3
3
  export type {
4
4
  AskGuidedReviewInput,
5
+ EditGuidedReviewDraftInput,
6
+ GuidedReviewPostResult,
7
+ PostGuidedReviewDraftsInput,
5
8
  GuidedReviewChange,
6
9
  GuidedReviewCommentDraft,
10
+ GuidedReviewDraftReport,
11
+ GuidedReviewFailure,
7
12
  GuidedReviewExchange,
8
13
  GuidedReviewMessage,
9
14
  GuidedReviewSession,