@cat-factory/app 0.305.0 → 0.307.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 +56 -0
- package/app/components/guidedReview/GuidedReview.logic.ts +39 -1
- package/app/components/guidedReview/GuidedReviewDraftCard.vue +145 -0
- package/app/components/guidedReview/GuidedReviewThread.vue +101 -11
- package/app/composables/api/guidedReview.ts +27 -0
- package/app/composables/usePipelineErrorToast.ts +8 -0
- package/app/stores/guidedReview.ts +28 -0
- package/app/types/guided-review.ts +3 -0
- package/i18n/locales/de.json +33 -5
- package/i18n/locales/en.json +33 -5
- package/i18n/locales/es.json +33 -5
- package/i18n/locales/fr.json +33 -5
- package/i18n/locales/he.json +33 -5
- package/i18n/locales/it.json +33 -5
- package/i18n/locales/ja.json +33 -5
- package/i18n/locales/pl.json +33 -5
- package/i18n/locales/tr.json +33 -5
- package/i18n/locales/uk.json +33 -5
- package/package.json +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { GUIDED_REVIEW_POST_LEASE_MS } from '@cat-factory/contracts'
|
|
2
3
|
import {
|
|
3
4
|
GUIDED_REVIEW_FAILURE_REASONS,
|
|
4
5
|
guidedReviewDroppedDraftReasonSchema,
|
|
@@ -6,6 +7,9 @@ import {
|
|
|
6
7
|
import en from '../../../i18n/locales/en.json'
|
|
7
8
|
import {
|
|
8
9
|
NEW_THREAD_TAB,
|
|
10
|
+
draftEdit,
|
|
11
|
+
isEditableDraft,
|
|
12
|
+
postableSelection,
|
|
9
13
|
canAsk,
|
|
10
14
|
citationLabel,
|
|
11
15
|
draftAnchor,
|
|
@@ -121,3 +125,55 @@ describe('isStale', () => {
|
|
|
121
125
|
expect(isStale('a', null)).toBe(false)
|
|
122
126
|
})
|
|
123
127
|
})
|
|
128
|
+
|
|
129
|
+
describe('draft editing and posting', () => {
|
|
130
|
+
it('treats proposed and failed drafts as still editable', () => {
|
|
131
|
+
expect(
|
|
132
|
+
['proposed', 'failed', 'posting', 'posted', 'discarded'].map((status) =>
|
|
133
|
+
isEditableDraft({ status: status as never }),
|
|
134
|
+
),
|
|
135
|
+
).toEqual([true, true, false, false, false])
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('posts only the selected drafts that are still postable, in list order', () => {
|
|
139
|
+
const drafts = [
|
|
140
|
+
{ id: 'a', status: 'proposed' as const, updatedAt: 0 },
|
|
141
|
+
{ id: 'b', status: 'posted' as const, updatedAt: 0 },
|
|
142
|
+
{ id: 'c', status: 'failed' as const, updatedAt: 0 },
|
|
143
|
+
]
|
|
144
|
+
expect(postableSelection(drafts, new Set(['c', 'b', 'a']), 1)).toEqual(['a', 'c'])
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('offers a posting draft again only once its claim outlives the lease', () => {
|
|
148
|
+
const stranded = [{ id: 'p', status: 'posting' as const, updatedAt: 1_000 }]
|
|
149
|
+
const selected = new Set(['p'])
|
|
150
|
+
expect(postableSelection(stranded, selected, 1_000 + GUIDED_REVIEW_POST_LEASE_MS)).toEqual([])
|
|
151
|
+
expect(postableSelection(stranded, selected, 1_001 + GUIDED_REVIEW_POST_LEASE_MS)).toEqual([
|
|
152
|
+
'p',
|
|
153
|
+
])
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('sends only what an edit changed, and nothing for an untouched form', () => {
|
|
157
|
+
const draft = { body: 'Bound the retries.', line: 3, startLine: null, side: 'RIGHT' as const }
|
|
158
|
+
expect(draftEdit(draft, { body: ' Bound the retries. ', line: 3, side: 'RIGHT' })).toEqual({})
|
|
159
|
+
expect(draftEdit(draft, { body: 'Cap at three.', line: 4, side: 'RIGHT' })).toEqual({
|
|
160
|
+
body: 'Cap at three.',
|
|
161
|
+
line: 4,
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('drops the start of a multi-line draft only when the edit moves it', () => {
|
|
166
|
+
const span = { body: 'Span.', line: 8, startLine: 5, side: 'RIGHT' as const }
|
|
167
|
+
expect(draftEdit(span, { body: 'Reworded.', line: 8, side: 'RIGHT' })).toEqual({
|
|
168
|
+
body: 'Reworded.',
|
|
169
|
+
})
|
|
170
|
+
expect(draftEdit(span, { body: 'Span.', line: 3, side: 'RIGHT' })).toEqual({
|
|
171
|
+
line: 3,
|
|
172
|
+
startLine: null,
|
|
173
|
+
})
|
|
174
|
+
expect(draftEdit(span, { body: 'Span.', line: 8, side: 'LEFT' })).toEqual({
|
|
175
|
+
startLine: null,
|
|
176
|
+
side: 'LEFT',
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
})
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isPostableDraft,
|
|
3
|
+
resolvePrNumber,
|
|
4
|
+
type GuidedReviewFailureReason,
|
|
5
|
+
} from '@cat-factory/contracts'
|
|
2
6
|
import type {
|
|
7
|
+
GuidedReviewCommentDraft,
|
|
3
8
|
GuidedReviewMessage,
|
|
4
9
|
GuidedReviewSessionView,
|
|
5
10
|
OpenGuidedReviewInput,
|
|
@@ -106,3 +111,36 @@ export function isStale(
|
|
|
106
111
|
): boolean {
|
|
107
112
|
return !!latestHeadSha && latestHeadSha !== reviewedHeadSha
|
|
108
113
|
}
|
|
114
|
+
|
|
115
|
+
/** A draft the reviewer may still edit, discard or post: `proposed`, or a `failed` post. */
|
|
116
|
+
export function isEditableDraft(draft: Pick<GuidedReviewCommentDraft, 'status'>): boolean {
|
|
117
|
+
return draft.status === 'proposed' || draft.status === 'failed'
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The selected drafts a post may claim at `now`, in the order they are listed. */
|
|
121
|
+
export function postableSelection(
|
|
122
|
+
drafts: Pick<GuidedReviewCommentDraft, 'id' | 'status' | 'updatedAt'>[],
|
|
123
|
+
selected: ReadonlySet<string>,
|
|
124
|
+
now: number,
|
|
125
|
+
): string[] {
|
|
126
|
+
return drafts.filter((d) => selected.has(d.id) && isPostableDraft(d, now)).map((d) => d.id)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The fields an inline edit actually changed, against the draft as loaded. Empty when nothing
|
|
131
|
+
* moved, so the editor saves nothing rather than bumping the draft's revision for no change. The
|
|
132
|
+
* editor places a single line, so moving a multi-line draft drops its start: the server would
|
|
133
|
+
* otherwise keep the old start against the new line and refuse the span.
|
|
134
|
+
*/
|
|
135
|
+
export function draftEdit(
|
|
136
|
+
draft: Pick<GuidedReviewCommentDraft, 'body' | 'line' | 'startLine' | 'side'>,
|
|
137
|
+
form: { body: string; line: number; side: 'LEFT' | 'RIGHT' },
|
|
138
|
+
): { body?: string; line?: number; startLine?: null; side?: 'LEFT' | 'RIGHT' } {
|
|
139
|
+
const moved = form.line !== draft.line || form.side !== draft.side
|
|
140
|
+
return {
|
|
141
|
+
...(form.body.trim() !== draft.body ? { body: form.body.trim() } : {}),
|
|
142
|
+
...(form.line !== draft.line ? { line: form.line } : {}),
|
|
143
|
+
...(moved && draft.startLine !== null ? { startLine: null } : {}),
|
|
144
|
+
...(form.side !== draft.side ? { side: form.side } : {}),
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// One comment draft: its anchor, body and rationale, with selection for posting and an inline
|
|
3
|
+
// editor. Only a `proposed` or `failed` draft is editable, though a `posting` one whose poster died
|
|
4
|
+
// can be selected for posting again; the server refuses a stale edit as
|
|
5
|
+
// `draft_conflict` and a line outside the diff, both reported through the error funnel.
|
|
6
|
+
import type { GuidedReviewCommentDraft } from '~/types/domain'
|
|
7
|
+
import MarkdownProse from '~/components/common/MarkdownProse.vue'
|
|
8
|
+
import {
|
|
9
|
+
draftAnchor,
|
|
10
|
+
draftEdit,
|
|
11
|
+
isEditableDraft,
|
|
12
|
+
} from '~/components/guidedReview/GuidedReview.logic'
|
|
13
|
+
|
|
14
|
+
const props = defineProps<{
|
|
15
|
+
sessionId: string
|
|
16
|
+
draft: GuidedReviewCommentDraft
|
|
17
|
+
selected: boolean
|
|
18
|
+
/** Whether a post may claim this draft now, including a stranded `posting` one. */
|
|
19
|
+
postable: boolean
|
|
20
|
+
}>()
|
|
21
|
+
const emit = defineEmits<{ 'update:selected': [value: boolean] }>()
|
|
22
|
+
const { t } = useI18n()
|
|
23
|
+
const store = useGuidedReviewStore()
|
|
24
|
+
const { present } = usePipelineErrorToast()
|
|
25
|
+
|
|
26
|
+
const editing = ref(false)
|
|
27
|
+
const saving = ref(false)
|
|
28
|
+
const form = reactive({ body: '', line: 1, side: 'RIGHT' as 'LEFT' | 'RIGHT' })
|
|
29
|
+
const editable = computed(() => isEditableDraft(props.draft))
|
|
30
|
+
const STATUS_COLOR = {
|
|
31
|
+
proposed: 'neutral',
|
|
32
|
+
posting: 'info',
|
|
33
|
+
posted: 'success',
|
|
34
|
+
failed: 'error',
|
|
35
|
+
discarded: 'neutral',
|
|
36
|
+
} as const
|
|
37
|
+
|
|
38
|
+
function startEdit(): void {
|
|
39
|
+
form.body = props.draft.body
|
|
40
|
+
form.line = props.draft.line
|
|
41
|
+
form.side = props.draft.side
|
|
42
|
+
editing.value = true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function save(): Promise<void> {
|
|
46
|
+
const changes = draftEdit(props.draft, form)
|
|
47
|
+
if (Object.keys(changes).length === 0) {
|
|
48
|
+
editing.value = false
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
saving.value = true
|
|
52
|
+
try {
|
|
53
|
+
await store.editDraft(props.sessionId, props.draft.id, { rev: props.draft.rev, ...changes })
|
|
54
|
+
editing.value = false
|
|
55
|
+
} catch (error) {
|
|
56
|
+
present(error, 'guidedReview.errors.editDraft')
|
|
57
|
+
} finally {
|
|
58
|
+
saving.value = false
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function discard(): Promise<void> {
|
|
63
|
+
saving.value = true
|
|
64
|
+
try {
|
|
65
|
+
await store.editDraft(props.sessionId, props.draft.id, { rev: props.draft.rev, discard: true })
|
|
66
|
+
emit('update:selected', false)
|
|
67
|
+
} catch (error) {
|
|
68
|
+
present(error, 'guidedReview.errors.editDraft')
|
|
69
|
+
} finally {
|
|
70
|
+
saving.value = false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<div
|
|
77
|
+
class="rounded-md border border-default p-2 text-sm"
|
|
78
|
+
:class="{ 'opacity-60': draft.status === 'discarded' }"
|
|
79
|
+
data-testid="guided-review-draft"
|
|
80
|
+
>
|
|
81
|
+
<div class="flex items-center gap-2">
|
|
82
|
+
<UCheckbox
|
|
83
|
+
v-if="postable"
|
|
84
|
+
:model-value="selected"
|
|
85
|
+
:aria-label="t('guidedReview.drafts.select')"
|
|
86
|
+
@update:model-value="emit('update:selected', $event === true)"
|
|
87
|
+
/>
|
|
88
|
+
<p class="flex-1 text-xs text-dimmed">
|
|
89
|
+
{{ draftAnchor(draft) }} ({{ t(`guidedReview.drafts.side.${draft.side}`) }})
|
|
90
|
+
</p>
|
|
91
|
+
<UBadge size="sm" variant="subtle" :color="STATUS_COLOR[draft.status]">
|
|
92
|
+
{{ t(`guidedReview.drafts.status.${draft.status}`) }}
|
|
93
|
+
</UBadge>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div v-if="editing" class="mt-2 space-y-2">
|
|
97
|
+
<UTextarea v-model="form.body" :rows="3" autoresize class="w-full" />
|
|
98
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
99
|
+
<UInputNumber
|
|
100
|
+
v-model="form.line"
|
|
101
|
+
:min="1"
|
|
102
|
+
size="sm"
|
|
103
|
+
:aria-label="t('guidedReview.drafts.line')"
|
|
104
|
+
/>
|
|
105
|
+
<USelect
|
|
106
|
+
v-model="form.side"
|
|
107
|
+
size="sm"
|
|
108
|
+
:items="[
|
|
109
|
+
{ label: t('guidedReview.drafts.side.RIGHT'), value: 'RIGHT' },
|
|
110
|
+
{ label: t('guidedReview.drafts.side.LEFT'), value: 'LEFT' },
|
|
111
|
+
]"
|
|
112
|
+
:aria-label="t('guidedReview.drafts.sideLabel')"
|
|
113
|
+
/>
|
|
114
|
+
<UButton size="sm" color="primary" :loading="saving" @click="save">
|
|
115
|
+
{{ t('guidedReview.drafts.save') }}
|
|
116
|
+
</UButton>
|
|
117
|
+
<UButton size="sm" variant="ghost" :disabled="saving" @click="editing = false">
|
|
118
|
+
{{ t('guidedReview.drafts.cancel') }}
|
|
119
|
+
</UButton>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
<template v-else>
|
|
123
|
+
<MarkdownProse class="mt-1" :text="draft.body" />
|
|
124
|
+
<p v-if="draft.rationale" class="mt-1 text-xs text-muted">{{ draft.rationale }}</p>
|
|
125
|
+
<p v-if="draft.status === 'failed' && draft.postError" class="mt-1 text-xs text-error">
|
|
126
|
+
{{ t('guidedReview.drafts.postFailed') }}: {{ draft.postError }}
|
|
127
|
+
</p>
|
|
128
|
+
<div v-if="editable" class="mt-2 flex gap-2">
|
|
129
|
+
<UButton size="xs" variant="ghost" icon="i-lucide-pencil" @click="startEdit">
|
|
130
|
+
{{ t('guidedReview.drafts.edit') }}
|
|
131
|
+
</UButton>
|
|
132
|
+
<UButton
|
|
133
|
+
size="xs"
|
|
134
|
+
variant="ghost"
|
|
135
|
+
color="neutral"
|
|
136
|
+
icon="i-lucide-trash-2"
|
|
137
|
+
:loading="saving"
|
|
138
|
+
@click="discard"
|
|
139
|
+
>
|
|
140
|
+
{{ t('guidedReview.drafts.discard') }}
|
|
141
|
+
</UButton>
|
|
142
|
+
</div>
|
|
143
|
+
</template>
|
|
144
|
+
</div>
|
|
145
|
+
</template>
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// One exploration thread: its messages, the comment drafts it produced, and a composer. While its
|
|
3
3
|
// own answer is pending the composer waits; other threads are unaffected.
|
|
4
|
+
import { isPostableDraft } from '@cat-factory/contracts'
|
|
4
5
|
import type { GuidedReviewCommentDraft, GuidedReviewThreadView } from '~/types/domain'
|
|
5
6
|
import MarkdownProse from '~/components/common/MarkdownProse.vue'
|
|
6
7
|
import GuidedReviewFailure from '~/components/guidedReview/GuidedReviewFailure.vue'
|
|
8
|
+
import GuidedReviewDraftCard from '~/components/guidedReview/GuidedReviewDraftCard.vue'
|
|
7
9
|
import {
|
|
8
10
|
canAsk,
|
|
9
11
|
citationLabel,
|
|
10
|
-
draftAnchor,
|
|
11
12
|
droppedAnchor,
|
|
12
13
|
isLive,
|
|
13
14
|
keptDrafts,
|
|
15
|
+
postableSelection,
|
|
14
16
|
} from '~/components/guidedReview/GuidedReview.logic'
|
|
15
17
|
|
|
16
18
|
const props = defineProps<{
|
|
@@ -25,6 +27,7 @@ const store = useGuidedReviewStore()
|
|
|
25
27
|
const { present } = usePipelineErrorToast()
|
|
26
28
|
|
|
27
29
|
const prompt = ref('')
|
|
30
|
+
const deep = ref(false)
|
|
28
31
|
const sending = ref(false)
|
|
29
32
|
const messages = computed(() => props.view?.messages ?? [])
|
|
30
33
|
const ready = computed(() => canAsk(messages.value) && !sending.value)
|
|
@@ -35,10 +38,11 @@ async function send(): Promise<void> {
|
|
|
35
38
|
const content = prompt.value.trim()
|
|
36
39
|
sending.value = true
|
|
37
40
|
try {
|
|
41
|
+
const question = { content, ...(deep.value ? { depth: 'deep' as const } : {}) }
|
|
38
42
|
if (props.view) {
|
|
39
|
-
await store.ask(props.sessionId, props.view.thread.id,
|
|
43
|
+
await store.ask(props.sessionId, props.view.thread.id, question)
|
|
40
44
|
} else {
|
|
41
|
-
const created = await store.openThread(props.sessionId, { question
|
|
45
|
+
const created = await store.openThread(props.sessionId, { question })
|
|
42
46
|
emit('created', created.thread.id)
|
|
43
47
|
}
|
|
44
48
|
prompt.value = ''
|
|
@@ -49,6 +53,49 @@ async function send(): Promise<void> {
|
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
const selected = ref(new Set<string>())
|
|
57
|
+
const summary = ref('')
|
|
58
|
+
const posting = ref(false)
|
|
59
|
+
const postResult = ref<{
|
|
60
|
+
posted: number
|
|
61
|
+
failed: number
|
|
62
|
+
skipped: number
|
|
63
|
+
summaryError: string | null
|
|
64
|
+
} | null>(null)
|
|
65
|
+
// A `posting` draft becomes postable again once its claim outlives the lease, so the clock only
|
|
66
|
+
// runs while one is on screen.
|
|
67
|
+
const now = useNowTick(30_000, () => props.drafts.some((d) => d.status === 'posting'))
|
|
68
|
+
const toPost = computed(() => postableSelection(props.drafts, selected.value, now.value))
|
|
69
|
+
|
|
70
|
+
function setSelected(id: string, value: boolean): void {
|
|
71
|
+
const next = new Set(selected.value)
|
|
72
|
+
if (value) next.add(id)
|
|
73
|
+
else next.delete(id)
|
|
74
|
+
selected.value = next
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function post(): Promise<void> {
|
|
78
|
+
if (!toPost.value.length) return
|
|
79
|
+
posting.value = true
|
|
80
|
+
try {
|
|
81
|
+
const result = await store.postDrafts(props.sessionId, toPost.value, summary.value)
|
|
82
|
+
const summaryFailed = result.summary.posted === false
|
|
83
|
+
postResult.value = {
|
|
84
|
+
posted: result.posted,
|
|
85
|
+
failed: result.failed,
|
|
86
|
+
skipped: result.skipped.length,
|
|
87
|
+
summaryError: summaryFailed ? (result.summary.error ?? '') : null,
|
|
88
|
+
}
|
|
89
|
+
selected.value = new Set()
|
|
90
|
+
// A summary the host refused stays in the box, so the reviewer can post it with a retry.
|
|
91
|
+
if (!summaryFailed) summary.value = ''
|
|
92
|
+
} catch (error) {
|
|
93
|
+
present(error, 'guidedReview.errors.postDrafts')
|
|
94
|
+
} finally {
|
|
95
|
+
posting.value = false
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
52
99
|
async function draftComments(): Promise<void> {
|
|
53
100
|
if (!props.view || !ready.value) return
|
|
54
101
|
sending.value = true
|
|
@@ -83,7 +130,11 @@ async function draftComments(): Promise<void> {
|
|
|
83
130
|
</template>
|
|
84
131
|
<p v-else-if="isLive(m)" class="flex items-center gap-2 text-muted">
|
|
85
132
|
<UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
|
|
86
|
-
{{
|
|
133
|
+
{{
|
|
134
|
+
m.depth === 'deep'
|
|
135
|
+
? t('guidedReview.thread.investigating')
|
|
136
|
+
: t('guidedReview.thread.waiting')
|
|
137
|
+
}}
|
|
87
138
|
</p>
|
|
88
139
|
<GuidedReviewFailure v-else-if="m.status === 'failed' && m.failure" :failure="m.failure" />
|
|
89
140
|
<template v-else-if="m.kind === 'answer'">
|
|
@@ -126,13 +177,45 @@ async function draftComments(): Promise<void> {
|
|
|
126
177
|
<h4 class="text-xs font-semibold uppercase text-dimmed">
|
|
127
178
|
{{ t('guidedReview.drafts.title') }}
|
|
128
179
|
</h4>
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
180
|
+
<GuidedReviewDraftCard
|
|
181
|
+
v-for="d in drafts"
|
|
182
|
+
:key="d.id"
|
|
183
|
+
:session-id="sessionId"
|
|
184
|
+
:draft="d"
|
|
185
|
+
:selected="selected.has(d.id)"
|
|
186
|
+
:postable="isPostableDraft(d, now)"
|
|
187
|
+
@update:selected="setSelected(d.id, $event)"
|
|
188
|
+
/>
|
|
189
|
+
<div class="space-y-2 rounded-md bg-elevated p-2" data-testid="guided-review-post">
|
|
190
|
+
<UTextarea
|
|
191
|
+
v-model="summary"
|
|
192
|
+
:rows="2"
|
|
193
|
+
autoresize
|
|
194
|
+
class="w-full"
|
|
195
|
+
:placeholder="t('guidedReview.drafts.summaryPlaceholder')"
|
|
196
|
+
/>
|
|
197
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
198
|
+
<UButton
|
|
199
|
+
color="primary"
|
|
200
|
+
icon="i-lucide-upload"
|
|
201
|
+
:loading="posting"
|
|
202
|
+
:disabled="!toPost.length"
|
|
203
|
+
data-testid="guided-review-post-drafts"
|
|
204
|
+
@click="post"
|
|
205
|
+
>
|
|
206
|
+
{{ t('guidedReview.drafts.post', { count: toPost.length }) }}
|
|
207
|
+
</UButton>
|
|
208
|
+
<p v-if="postResult" class="text-xs text-muted" data-testid="guided-review-post-result">
|
|
209
|
+
{{ t('guidedReview.drafts.postResult', postResult) }}
|
|
210
|
+
</p>
|
|
211
|
+
<p
|
|
212
|
+
v-if="postResult && postResult.summaryError !== null"
|
|
213
|
+
class="text-xs text-error"
|
|
214
|
+
data-testid="guided-review-post-summary-error"
|
|
215
|
+
>
|
|
216
|
+
{{ t('guidedReview.drafts.summaryFailed') }}: {{ postResult.summaryError }}
|
|
217
|
+
</p>
|
|
218
|
+
</div>
|
|
136
219
|
</div>
|
|
137
220
|
</div>
|
|
138
221
|
</div>
|
|
@@ -151,6 +234,13 @@ async function draftComments(): Promise<void> {
|
|
|
151
234
|
@keydown.enter.ctrl.prevent="send"
|
|
152
235
|
/>
|
|
153
236
|
<div class="flex flex-wrap items-center gap-2">
|
|
237
|
+
<USwitch
|
|
238
|
+
v-model="deep"
|
|
239
|
+
size="sm"
|
|
240
|
+
:label="t('guidedReview.thread.deep')"
|
|
241
|
+
:description="t('guidedReview.thread.deepHint')"
|
|
242
|
+
data-testid="guided-review-deep"
|
|
243
|
+
/>
|
|
154
244
|
<UButton
|
|
155
245
|
color="primary"
|
|
156
246
|
icon="i-lucide-send"
|
|
@@ -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
|
}
|
|
@@ -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',
|
|
@@ -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,
|
|
@@ -2,6 +2,9 @@
|
|
|
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,
|
|
7
10
|
GuidedReviewDraftReport,
|
package/i18n/locales/de.json
CHANGED
|
@@ -4648,7 +4648,10 @@
|
|
|
4648
4648
|
"placeholder": "Stell eine Frage zur Änderung",
|
|
4649
4649
|
"busy": "Warte auf die Antwort auf die vorherige Frage",
|
|
4650
4650
|
"send": "Fragen",
|
|
4651
|
-
"draftRequest": "Review-Kommentare angefordert"
|
|
4651
|
+
"draftRequest": "Review-Kommentare angefordert",
|
|
4652
|
+
"deep": "Tiefenanalyse",
|
|
4653
|
+
"deepHint": "Antwort aus einem Checkout des Repositorys: langsamer, kann aber den ganzen Baum durchsuchen.",
|
|
4654
|
+
"investigating": "Untersuchung in einem Checkout des Repositorys. Das kann einige Minuten dauern."
|
|
4652
4655
|
},
|
|
4653
4656
|
"drafts": {
|
|
4654
4657
|
"title": "Kommentarentwürfe",
|
|
@@ -4657,7 +4660,26 @@
|
|
|
4657
4660
|
"side": {
|
|
4658
4661
|
"RIGHT": "neuer Code",
|
|
4659
4662
|
"LEFT": "alter Code"
|
|
4660
|
-
}
|
|
4663
|
+
},
|
|
4664
|
+
"select": "Diesen Entwurf zum Posten auswählen",
|
|
4665
|
+
"status": {
|
|
4666
|
+
"proposed": "Entwurf",
|
|
4667
|
+
"posting": "Wird gepostet",
|
|
4668
|
+
"posted": "Gepostet",
|
|
4669
|
+
"failed": "Nicht gepostet",
|
|
4670
|
+
"discarded": "Verworfen"
|
|
4671
|
+
},
|
|
4672
|
+
"edit": "Bearbeiten",
|
|
4673
|
+
"save": "Speichern",
|
|
4674
|
+
"cancel": "Abbrechen",
|
|
4675
|
+
"discard": "Verwerfen",
|
|
4676
|
+
"line": "Zeile",
|
|
4677
|
+
"sideLabel": "Seite",
|
|
4678
|
+
"postFailed": "Der Host hat diesen Kommentar abgelehnt",
|
|
4679
|
+
"summaryPlaceholder": "Optionaler Zusammenfassungskommentar für den Pull Request",
|
|
4680
|
+
"summaryFailed": "Der Host hat den Zusammenfassungskommentar abgelehnt",
|
|
4681
|
+
"post": "{count} im Pull Request posten",
|
|
4682
|
+
"postResult": "{posted} gepostet, {failed} nicht gepostet, {skipped} bereits erledigt."
|
|
4661
4683
|
},
|
|
4662
4684
|
"errors": {
|
|
4663
4685
|
"load": "Das Review konnte nicht geladen werden",
|
|
@@ -4665,7 +4687,9 @@
|
|
|
4665
4687
|
"open": "Das Review konnte nicht geöffnet werden",
|
|
4666
4688
|
"ask": "Die Frage konnte nicht gestellt werden",
|
|
4667
4689
|
"drafts": "Kommentarentwürfe konnten nicht angefordert werden",
|
|
4668
|
-
"refresh": "Das Review konnte nicht aktualisiert werden"
|
|
4690
|
+
"refresh": "Das Review konnte nicht aktualisiert werden",
|
|
4691
|
+
"editDraft": "Der Entwurf konnte nicht gespeichert werden",
|
|
4692
|
+
"postDrafts": "Die Entwürfe konnten nicht gepostet werden"
|
|
4669
4693
|
}
|
|
4670
4694
|
},
|
|
4671
4695
|
"pipeline": {
|
|
@@ -6392,7 +6416,9 @@
|
|
|
6392
6416
|
"delegated_executor_unwired": "Externer Executor nicht registriert",
|
|
6393
6417
|
"delegated_step_async_only": "Dieser Schritt wird abgefragt, nicht abgewartet",
|
|
6394
6418
|
"delegated_claim_missing": "Für diesen Schritt gibt es keine externe Arbeit zu prüfen",
|
|
6395
|
-
"thread_busy": "Dieser Thread antwortet noch"
|
|
6419
|
+
"thread_busy": "Dieser Thread antwortet noch",
|
|
6420
|
+
"draft_conflict": "Dieser Entwurf hat sich geändert",
|
|
6421
|
+
"session_stale": "Der Pull Request hat sich weiterentwickelt"
|
|
6396
6422
|
},
|
|
6397
6423
|
"description": {
|
|
6398
6424
|
"dependencies_unmet": "Diese Aufgabe hängt von anderen ab, die noch nicht abgeschlossen sind. Schließe sie ab oder gib sie frei und starte dann erneut.",
|
|
@@ -6450,7 +6476,9 @@
|
|
|
6450
6476
|
"delegated_executor_unwired": "Ein Schritt dieser Pipeline läuft auf einem externen Executor, den diese Installation nicht registriert; es gibt also kein Ziel für die Arbeit. Registriere den Executor im Code oder wähle für den Schritt einen, der hier läuft.",
|
|
6451
6477
|
"delegated_step_async_only": "Dieser Schritt läuft auf einem externen Executor und kann Stunden dauern. Er wird deshalb angestoßen und danach abgefragt, statt innerhalb einer Anfrage ausgeführt zu werden. Starte den Lauf über das Board; wenn du aus eigenem Code hierherkommst, steuere den Schritt über den asynchronen Pfad.",
|
|
6452
6478
|
"delegated_claim_missing": "Der Eintrag des Laufs darüber, was an den externen Executor geschickt wurde, fehlt, also gibt es nichts abzufragen. Am Executor selbst ist nichts falsch. Lade den Lauf neu und starte den Schritt erneut, falls er weiterhin hängt.",
|
|
6453
|
-
"thread_busy": "Die vorherige Frage in diesem Thread ist noch nicht beantwortet. Warte auf die Antwort oder frag in einem anderen Thread."
|
|
6479
|
+
"thread_busy": "Die vorherige Frage in diesem Thread ist noch nicht beantwortet. Warte auf die Antwort oder frag in einem anderen Thread.",
|
|
6480
|
+
"draft_conflict": "Dieser Kommentarentwurf wurde seit dem Laden bearbeitet, gepostet oder verworfen. Lade die Entwürfe neu und nimm deine Änderung erneut vor.",
|
|
6481
|
+
"session_stale": "Nach der Vorbereitung dieser Review kamen neue Commits hinzu, daher zeigen die Entwürfe möglicherweise auf falsche Zeilen. Aktualisiere die Review, prüfe die Entwürfe und poste dann."
|
|
6454
6482
|
},
|
|
6455
6483
|
"action": {
|
|
6456
6484
|
"connectGitHub": "GitHub verbinden",
|