@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.
@@ -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 { resolvePrNumber, type GuidedReviewFailureReason } from '@cat-factory/contracts'
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, { content })
43
+ await store.ask(props.sessionId, props.view.thread.id, question)
40
44
  } else {
41
- const created = await store.openThread(props.sessionId, { question: { content } })
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
- {{ t('guidedReview.thread.waiting') }}
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
- <div v-for="d in drafts" :key="d.id" class="rounded-md border border-default p-2 text-sm">
130
- <p class="text-xs text-dimmed">
131
- {{ draftAnchor(d) }}
132
- ({{ t(`guidedReview.drafts.side.${d.side}`) }})
133
- </p>
134
- <MarkdownProse :text="d.body" />
135
- <p v-if="d.rationale" class="mt-1 text-xs text-muted">{{ d.rationale }}</p>
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,
@@ -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",