@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.
- package/app/components/guidedReview/GuidedReview.logic.spec.ts +179 -0
- package/app/components/guidedReview/GuidedReview.logic.ts +146 -0
- package/app/components/guidedReview/GuidedReviewDraftCard.vue +145 -0
- package/app/components/guidedReview/GuidedReviewFailure.vue +37 -0
- package/app/components/guidedReview/GuidedReviewModal.vue +243 -0
- package/app/components/guidedReview/GuidedReviewOverview.vue +160 -0
- package/app/components/guidedReview/GuidedReviewPicker.vue +95 -0
- package/app/components/guidedReview/GuidedReviewThread.vue +254 -0
- package/app/components/panels/inspector/TaskReviewTarget.vue +36 -0
- package/app/composables/api/guidedReview.ts +27 -0
- package/app/composables/useNavContributions.ts +1 -0
- package/app/composables/usePipelineErrorToast.ts +8 -0
- package/app/modular/nav-contributions.ts +20 -0
- package/app/pages/index.vue +4 -0
- package/app/stores/guidedReview.ts +28 -0
- package/app/stores/ui/modals.ts +25 -1
- package/app/types/guided-review.ts +5 -0
- package/i18n/locales/de.json +105 -2
- package/i18n/locales/en.json +105 -2
- package/i18n/locales/es.json +105 -2
- package/i18n/locales/fr.json +105 -2
- package/i18n/locales/he.json +105 -2
- package/i18n/locales/it.json +105 -2
- package/i18n/locales/ja.json +105 -2
- package/i18n/locales/pl.json +105 -2
- package/i18n/locales/tr.json +105 -2
- package/i18n/locales/uk.json +105 -2
- package/package.json +2 -2
|
@@ -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',
|
package/app/pages/index.vue
CHANGED
|
@@ -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,
|
package/app/stores/ui/modals.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { ref } from 'vue'
|
|
2
|
-
import type {
|
|
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,
|