@cat-factory/app 0.303.4 → 0.305.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.
Files changed (34) hide show
  1. package/app/components/guidedReview/GuidedReview.logic.spec.ts +123 -0
  2. package/app/components/guidedReview/GuidedReview.logic.ts +108 -0
  3. package/app/components/guidedReview/GuidedReviewFailure.vue +37 -0
  4. package/app/components/guidedReview/GuidedReviewModal.vue +243 -0
  5. package/app/components/guidedReview/GuidedReviewOverview.vue +160 -0
  6. package/app/components/guidedReview/GuidedReviewPicker.vue +95 -0
  7. package/app/components/guidedReview/GuidedReviewThread.vue +177 -0
  8. package/app/components/panels/inspector/TaskReviewTarget.vue +36 -0
  9. package/app/composables/api/guidedReview.ts +80 -0
  10. package/app/composables/useApi.ts +2 -0
  11. package/app/composables/useNavContributions.ts +1 -0
  12. package/app/composables/usePipelineErrorToast.ts +4 -0
  13. package/app/composables/useWorkspaceStream.ts +4 -0
  14. package/app/composables/workspaceStream/applyWorkspaceEvent.spec.ts +2 -0
  15. package/app/composables/workspaceStream/applyWorkspaceEvent.ts +6 -0
  16. package/app/modular/nav-contributions.ts +20 -0
  17. package/app/pages/index.vue +4 -0
  18. package/app/stores/guidedReview.spec.ts +148 -0
  19. package/app/stores/guidedReview.ts +200 -0
  20. package/app/stores/ui/modals.ts +25 -1
  21. package/app/stores/workspace/hydrate.ts +2 -0
  22. package/app/types/domain.ts +1 -0
  23. package/app/types/guided-review.ts +16 -0
  24. package/i18n/locales/de.json +82 -2
  25. package/i18n/locales/en.json +82 -2
  26. package/i18n/locales/es.json +82 -2
  27. package/i18n/locales/fr.json +82 -2
  28. package/i18n/locales/he.json +82 -2
  29. package/i18n/locales/it.json +82 -2
  30. package/i18n/locales/ja.json +82 -2
  31. package/i18n/locales/pl.json +82 -2
  32. package/i18n/locales/tr.json +82 -2
  33. package/i18n/locales/uk.json +82 -2
  34. package/package.json +2 -2
@@ -0,0 +1,148 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { ApiError } from '~/composables/api/errors'
3
+ import { useGuidedReviewStore } from '~/stores/guidedReview'
4
+ import { useWorkspaceStore } from '~/stores/workspace'
5
+
6
+ function view(sessionId: string, title: string) {
7
+ return {
8
+ session: { id: sessionId, prTitle: title },
9
+ threads: [],
10
+ drafts: [],
11
+ } as never
12
+ }
13
+
14
+ function threadView(threadId: string, messages: number) {
15
+ return {
16
+ thread: { id: threadId, sessionId: 's1' },
17
+ messages: Array.from({ length: messages }, (_, i) => ({ id: `m${i}` })),
18
+ } as never
19
+ }
20
+
21
+ function deferred<T>() {
22
+ let resolve!: (value: T) => void
23
+ const promise = new Promise<T>((r) => (resolve = r))
24
+ return { promise, resolve }
25
+ }
26
+
27
+ describe('guided review store', () => {
28
+ it('refetches only what this client has loaded when a change arrives', async () => {
29
+ const getGuidedReview = vi.fn(async (_ws: string, id: string) => view(id, 'fresh'))
30
+ const getGuidedReviewThread = vi.fn(async () => threadView('t1', 2))
31
+ vi.stubGlobal('useApi', () => ({ getGuidedReview, getGuidedReviewThread }))
32
+ useWorkspaceStore().workspaceId = 'ws_1'
33
+ const store = useGuidedReviewStore()
34
+
35
+ await store.applyChange({ sessionId: 'elsewhere', scope: 'thread', threadId: 't9' })
36
+ expect(getGuidedReview).not.toHaveBeenCalled()
37
+ expect(getGuidedReviewThread).not.toHaveBeenCalled()
38
+
39
+ await store.loadSession('s1')
40
+ await store.loadThread('s1', 't1')
41
+ getGuidedReview.mockClear()
42
+ getGuidedReviewThread.mockClear()
43
+ await store.applyChange({ sessionId: 's1', scope: 'thread', threadId: 't1' })
44
+ expect(getGuidedReview).toHaveBeenCalledTimes(1)
45
+ expect(getGuidedReviewThread).toHaveBeenCalledTimes(1)
46
+ })
47
+
48
+ it('drops a slow reply that a newer fetch overtook', async () => {
49
+ const slow = deferred<never>()
50
+ const replies = [slow.promise, Promise.resolve(view('s1', 'newer'))]
51
+ vi.stubGlobal('useApi', () => ({ getGuidedReview: vi.fn(() => replies.shift()) }))
52
+ useWorkspaceStore().workspaceId = 'ws_1'
53
+ const store = useGuidedReviewStore()
54
+
55
+ const first = store.loadSession('s1')
56
+ await store.loadSession('s1')
57
+ slow.resolve(view('s1', 'stale'))
58
+ await first
59
+ expect(store.sessions.s1?.session.prTitle).toBe('newer')
60
+ })
61
+
62
+ it('lets a refresh win over a load that started before it', async () => {
63
+ const slow = deferred<never>()
64
+ vi.stubGlobal('useApi', () => ({
65
+ getGuidedReview: vi.fn(() => slow.promise),
66
+ refreshGuidedReview: vi.fn(async () => view('s1', 'refreshed')),
67
+ }))
68
+ useWorkspaceStore().workspaceId = 'ws_1'
69
+ const store = useGuidedReviewStore()
70
+
71
+ const load = store.loadSession('s1')
72
+ await store.refresh('s1')
73
+ slow.resolve(view('s1', 'stale'))
74
+ await load
75
+ expect(store.sessions.s1?.session.prTitle).toBe('refreshed')
76
+ })
77
+
78
+ it('does not bring back a deleted session through a load still in flight', async () => {
79
+ const slow = deferred<never>()
80
+ vi.stubGlobal('useApi', () => ({ getGuidedReview: vi.fn(() => slow.promise) }))
81
+ useWorkspaceStore().workspaceId = 'ws_1'
82
+ const store = useGuidedReviewStore()
83
+
84
+ const load = store.loadSession('s1')
85
+ await store.applyChange({ sessionId: 's1', scope: 'deleted' })
86
+ slow.resolve(view('s1', 'stale'))
87
+ await load
88
+ expect(store.sessions.s1).toBeUndefined()
89
+ })
90
+
91
+ it('refetches the thread a drafts change names', async () => {
92
+ const getGuidedReview = vi.fn(async (_ws: string, id: string) => view(id, 'x'))
93
+ const getGuidedReviewThread = vi.fn(async () => threadView('t1', 3))
94
+ vi.stubGlobal('useApi', () => ({ getGuidedReview, getGuidedReviewThread }))
95
+ useWorkspaceStore().workspaceId = 'ws_1'
96
+ const store = useGuidedReviewStore()
97
+ await store.loadSession('s1')
98
+ await store.loadThread('s1', 't1')
99
+ getGuidedReviewThread.mockClear()
100
+ await store.applyChange({ sessionId: 's1', scope: 'drafts', threadId: 't1' })
101
+ expect(getGuidedReviewThread).toHaveBeenCalledTimes(1)
102
+ })
103
+
104
+ it('records a failed background refetch and clears it when the session lands again', async () => {
105
+ const failure = new ApiError(503, { error: { code: 'unavailable' } })
106
+ const getGuidedReview = vi
107
+ .fn()
108
+ .mockResolvedValueOnce(view('s1', 'x'))
109
+ .mockRejectedValueOnce(failure)
110
+ .mockResolvedValueOnce(view('s1', 'y'))
111
+ vi.stubGlobal('useApi', () => ({ getGuidedReview }))
112
+ useWorkspaceStore().workspaceId = 'ws_1'
113
+ const store = useGuidedReviewStore()
114
+ await store.loadSession('s1')
115
+ await store.applyChange({ sessionId: 's1', scope: 'session' })
116
+ expect(store.sessions.s1?.session.prTitle).toBe('x')
117
+ expect(store.refetchFailures.s1).toBe(failure)
118
+ await store.applyChange({ sessionId: 's1', scope: 'session' })
119
+ expect(store.refetchFailures.s1).toBeUndefined()
120
+ })
121
+
122
+ it('forgets a session whose refetch answers 404', async () => {
123
+ const getGuidedReview = vi
124
+ .fn()
125
+ .mockResolvedValueOnce(view('s1', 'x'))
126
+ .mockRejectedValueOnce(new ApiError(404, { error: { code: 'not_found' } }))
127
+ vi.stubGlobal('useApi', () => ({ getGuidedReview }))
128
+ useWorkspaceStore().workspaceId = 'ws_1'
129
+ const store = useGuidedReviewStore()
130
+ await store.loadSession('s1')
131
+ await store.applyChange({ sessionId: 's1', scope: 'session' })
132
+ expect(store.sessions.s1).toBeUndefined()
133
+ })
134
+
135
+ it('forgets a deleted session and its threads', async () => {
136
+ vi.stubGlobal('useApi', () => ({
137
+ getGuidedReview: vi.fn(async () => view('s1', 'x')),
138
+ getGuidedReviewThread: vi.fn(async () => threadView('t1', 1)),
139
+ }))
140
+ useWorkspaceStore().workspaceId = 'ws_1'
141
+ const store = useGuidedReviewStore()
142
+ await store.loadSession('s1')
143
+ await store.loadThread('s1', 't1')
144
+ await store.applyChange({ sessionId: 's1', scope: 'deleted' })
145
+ expect(store.sessions.s1).toBeUndefined()
146
+ expect(store.threads.t1).toBeUndefined()
147
+ })
148
+ })
@@ -0,0 +1,200 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref } from 'vue'
3
+ import type {
4
+ AskGuidedReviewInput,
5
+ GuidedReviewChange,
6
+ GuidedReviewSessionView,
7
+ GuidedReviewThreadView,
8
+ OpenGuidedReviewInput,
9
+ OpenGuidedReviewThreadInput,
10
+ } from '~/types/domain'
11
+ import { ApiError } from '~/composables/api/errors'
12
+ import { useWorkspaceStore } from '~/stores/workspace'
13
+
14
+ /**
15
+ * Guided PR review sessions and the threads a window has opened, loaded on demand and kept live
16
+ * by `guidedReview` stream events. An event carries ids only, so `applyChange` refetches what is
17
+ * loaded here and ignores the rest. Every fetch and every write takes a ticket, and a fetch lands
18
+ * only while its ticket is the newest for its key, so a change that arrives mid-fetch is never
19
+ * lost to a stale reply and a forgotten session is never brought back by one.
20
+ */
21
+ export const useGuidedReviewStore = defineStore('guidedReview', () => {
22
+ const api = useApi()
23
+ const workspace = useWorkspaceStore()
24
+
25
+ const sessions = ref<Record<string, GuidedReviewSessionView>>({})
26
+ const threads = ref<Record<string, GuidedReviewThreadView>>({})
27
+ /**
28
+ * The last error of a background refetch per session, cleared when that session or one of its
29
+ * threads lands again, so a window can say its view may be out of date.
30
+ */
31
+ const refetchFailures = ref<Record<string, unknown>>({})
32
+ // One counter for every key and never reset, so a key dropped by `forget` or `reset` is never
33
+ // landed on by a fetch that was in flight when it went.
34
+ let issued = 0
35
+ const newest = new Map<string, number>()
36
+
37
+ const sessionKey = (sessionId: string) => `s:${sessionId}`
38
+ const threadKey = (sessionId: string, threadId: string) => `t:${sessionId}:${threadId}`
39
+
40
+ function claim(key: string): number {
41
+ issued += 1
42
+ newest.set(key, issued)
43
+ return issued
44
+ }
45
+
46
+ /** Run `fetch` for `key`, landing its result only if no newer fetch or write for it started. */
47
+ async function latest<T>(key: string, fetch: () => Promise<T>, land: (value: T) => void) {
48
+ const ticket = claim(key)
49
+ const value = await fetch()
50
+ if (newest.get(key) === ticket) land(value)
51
+ }
52
+
53
+ function putSession(view: GuidedReviewSessionView) {
54
+ sessions.value[view.session.id] = view
55
+ delete refetchFailures.value[view.session.id]
56
+ }
57
+
58
+ function putThread(view: GuidedReviewThreadView) {
59
+ threads.value[view.thread.id] = view
60
+ delete refetchFailures.value[view.thread.sessionId]
61
+ }
62
+
63
+ function loadSession(sessionId: string): Promise<void> {
64
+ const ws = workspace.requireId()
65
+ return latest(sessionKey(sessionId), () => api.getGuidedReview(ws, sessionId), putSession)
66
+ }
67
+
68
+ function loadThread(sessionId: string, threadId: string): Promise<void> {
69
+ const ws = workspace.requireId()
70
+ return latest(
71
+ threadKey(sessionId, threadId),
72
+ () => api.getGuidedReviewThread(ws, sessionId, threadId),
73
+ putThread,
74
+ )
75
+ }
76
+
77
+ async function open(input: OpenGuidedReviewInput): Promise<GuidedReviewSessionView> {
78
+ const view = await api.openGuidedReview(workspace.requireId(), input)
79
+ claim(sessionKey(view.session.id))
80
+ putSession(view)
81
+ return view
82
+ }
83
+
84
+ function refresh(sessionId: string): Promise<void> {
85
+ const ws = workspace.requireId()
86
+ return latest(sessionKey(sessionId), () => api.refreshGuidedReview(ws, sessionId), putSession)
87
+ }
88
+
89
+ async function remove(sessionId: string): Promise<void> {
90
+ await api.deleteGuidedReview(workspace.requireId(), sessionId)
91
+ forget(sessionId)
92
+ }
93
+
94
+ async function openThread(
95
+ sessionId: string,
96
+ input: OpenGuidedReviewThreadInput,
97
+ ): Promise<GuidedReviewThreadView> {
98
+ const view = await api.openGuidedReviewThread(workspace.requireId(), sessionId, input)
99
+ claim(threadKey(sessionId, view.thread.id))
100
+ putThread(view)
101
+ await loadSession(sessionId)
102
+ return view
103
+ }
104
+
105
+ async function ask(sessionId: string, threadId: string, input: AskGuidedReviewInput) {
106
+ await api.askGuidedReview(workspace.requireId(), sessionId, threadId, input)
107
+ await Promise.all([loadThread(sessionId, threadId), loadSession(sessionId)])
108
+ }
109
+
110
+ async function requestDrafts(sessionId: string, threadId: string, instructions = '') {
111
+ await api.requestGuidedReviewDrafts(workspace.requireId(), sessionId, threadId, instructions)
112
+ await Promise.all([loadThread(sessionId, threadId), loadSession(sessionId)])
113
+ }
114
+
115
+ function forget(sessionId: string) {
116
+ delete sessions.value[sessionId]
117
+ delete refetchFailures.value[sessionId]
118
+ newest.delete(sessionKey(sessionId))
119
+ const threadPrefix = threadKey(sessionId, '')
120
+ for (const key of newest.keys()) {
121
+ if (key.startsWith(threadPrefix)) newest.delete(key)
122
+ }
123
+ for (const [id, view] of Object.entries(threads.value)) {
124
+ if (view.thread.sessionId === sessionId) delete threads.value[id]
125
+ }
126
+ }
127
+
128
+ /** Drop everything on a board switch; fetches still in flight for the old board never land. */
129
+ function reset() {
130
+ sessions.value = {}
131
+ threads.value = {}
132
+ refetchFailures.value = {}
133
+ newest.clear()
134
+ }
135
+
136
+ /**
137
+ * A refetch nobody awaits (a live event, a reconnect). A session that answers 404 is gone and
138
+ * is forgotten; any other failure keeps the last view and is recorded in `refetchFailures`.
139
+ */
140
+ async function follow(sessionId: string, load: () => Promise<void>): Promise<void> {
141
+ try {
142
+ await load()
143
+ } catch (error) {
144
+ if (error instanceof ApiError && error.statusCode === 404) {
145
+ forget(sessionId)
146
+ return
147
+ }
148
+ if (sessions.value[sessionId]) refetchFailures.value[sessionId] = error
149
+ }
150
+ }
151
+
152
+ /** Follow a live `guidedReview` event: refetch what this client has loaded, nothing else. */
153
+ async function applyChange(change: GuidedReviewChange): Promise<void> {
154
+ if (change.scope === 'deleted') {
155
+ forget(change.sessionId)
156
+ return
157
+ }
158
+ const { sessionId } = change
159
+ const loads: Promise<void>[] = []
160
+ // A thread change also moves the session's summary (its pending answer), so both refetch.
161
+ if (sessions.value[sessionId]) {
162
+ loads.push(follow(sessionId, () => loadSession(sessionId)))
163
+ }
164
+ if (change.scope === 'thread' || change.scope === 'drafts') {
165
+ const { threadId } = change
166
+ if (threads.value[threadId]) {
167
+ loads.push(follow(sessionId, () => loadThread(sessionId, threadId)))
168
+ }
169
+ }
170
+ await Promise.all(loads)
171
+ }
172
+
173
+ /** Refetch everything loaded here, for events missed while the stream was disconnected. */
174
+ async function resync(): Promise<void> {
175
+ const loads = [
176
+ ...Object.keys(sessions.value).map((id) => follow(id, () => loadSession(id))),
177
+ ...Object.values(threads.value).map(({ thread }) =>
178
+ follow(thread.sessionId, () => loadThread(thread.sessionId, thread.id)),
179
+ ),
180
+ ]
181
+ await Promise.all(loads)
182
+ }
183
+
184
+ return {
185
+ sessions,
186
+ threads,
187
+ refetchFailures,
188
+ loadSession,
189
+ loadThread,
190
+ open,
191
+ refresh,
192
+ remove,
193
+ openThread,
194
+ ask,
195
+ requestDrafts,
196
+ applyChange,
197
+ resync,
198
+ reset,
199
+ }
200
+ })
@@ -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,
@@ -10,6 +10,7 @@ import { useEnvironmentTestStore } from '~/stores/environmentTest'
10
10
  import { useExecutionStore } from '~/stores/execution'
11
11
  import { useFragmentsStore } from '~/stores/fragments'
12
12
  import { useGitHubStore } from '~/stores/github'
13
+ import { useGuidedReviewStore } from '~/stores/guidedReview'
13
14
  import { useInitiativesStore } from '~/stores/initiative'
14
15
  import { useKaizenStore } from '~/stores/kaizen'
15
16
  import { useObservabilityStore } from '~/stores/observability'
@@ -45,6 +46,7 @@ export function resetPerBoardCaches() {
45
46
  useGitHubStore().reset()
46
47
  useInitiativesStore().reset()
47
48
  useDocInterviewStore().reset()
49
+ useGuidedReviewStore().reset()
48
50
  // The per-RUN observability + Kaizen caches. An execution id belongs to the board that owns it
49
51
  // and neither store is part of the snapshot, so nothing else ever evicted a key: a session that
50
52
  // visited several boards kept every run it had ever opened a panel on.
@@ -258,3 +258,4 @@ export type * from './recurring'
258
258
  export type * from './tracker'
259
259
  export type * from './initiative'
260
260
  export type * from './doc-interview'
261
+ export type * from './guided-review'
@@ -0,0 +1,16 @@
1
+ // Guided PR review wire shapes, re-exported from the shared contracts package. The SPA imports
2
+ // these through `~/types/domain` like every other domain type.
3
+ export type {
4
+ AskGuidedReviewInput,
5
+ GuidedReviewChange,
6
+ GuidedReviewCommentDraft,
7
+ GuidedReviewDraftReport,
8
+ GuidedReviewFailure,
9
+ GuidedReviewExchange,
10
+ GuidedReviewMessage,
11
+ GuidedReviewSession,
12
+ GuidedReviewSessionView,
13
+ GuidedReviewThreadView,
14
+ OpenGuidedReviewInput,
15
+ OpenGuidedReviewThreadInput,
16
+ } from '@cat-factory/contracts'
@@ -2740,6 +2740,7 @@
2740
2740
  "cmd": {
2741
2741
  "newPipeline": "Eine Pipeline bauen",
2742
2742
  "assistant": "Assistenten öffnen",
2743
+ "guidedReview": "Einen Pull Request erkunden",
2743
2744
  "addFromRepo": "Service aus vorhandenem Repo hinzufügen",
2744
2745
  "bootstrapRepo": "Ein neues Repo bootstrappen",
2745
2746
  "githubManage": "GitHub-Verbindung verwalten",
@@ -2768,6 +2769,7 @@
2768
2769
  "keywords": {
2769
2770
  "newPipeline": "pipeline agents chain",
2770
2771
  "assistant": "ki assistent fragen eingabe für mich erledigen",
2772
+ "guidedReview": "geführtes review pull request pr erkunden erklären fragen",
2771
2773
  "addFromRepo": "github import existing",
2772
2774
  "bootstrapRepo": "scaffold create reference architecture",
2773
2775
  "github": "git repos pull requests issues",
@@ -4591,6 +4593,81 @@
4591
4593
  "unresolved_issue_service": "Der Assistent konnte nicht erkennen, unter welchem Service das angelegt werden soll. Nennen Sie den Service in Ihrer Anfrage."
4592
4594
  }
4593
4595
  },
4596
+ "guidedReview": {
4597
+ "title": "Geführtes Review",
4598
+ "titleFor": "{repo} #{number}: {title}",
4599
+ "loading": "Review wird geladen…",
4600
+ "outOfDate": "Live-Updates sind fehlgeschlagen, daher ist diese Ansicht möglicherweise veraltet.",
4601
+ "openFromTask": "Mit geführtem Review erkunden",
4602
+ "picker": {
4603
+ "intro": "Wähle ein Repository und einen Pull Request. Du erhältst einen Überblick über die Änderung und kannst in separaten Threads Fragen dazu stellen.",
4604
+ "loading": "Repositories werden geladen…",
4605
+ "noRepos": "Mit diesem Workspace sind noch keine Repositories verknüpft.",
4606
+ "repo": "Repository",
4607
+ "openPulls": "Offene Pull Requests",
4608
+ "number": "Nummer des Pull Requests",
4609
+ "open": "Review öffnen"
4610
+ },
4611
+ "overview": {
4612
+ "title": "Überblick",
4613
+ "stale": "Der Pull Request hat neue Commits",
4614
+ "refresh": "Aktualisieren",
4615
+ "working": "Überblick wird erstellt…",
4616
+ "intent": "Absicht",
4617
+ "changes": "Wesentliche Änderungen",
4618
+ "consequences": "Folgen",
4619
+ "risks": "Risiken",
4620
+ "focus": "Lohnt einen Blick",
4621
+ "suggested": "Vorgeschlagene Fragen"
4622
+ },
4623
+ "severity": {
4624
+ "low": "Niedrig",
4625
+ "medium": "Mittel",
4626
+ "high": "Hoch"
4627
+ },
4628
+ "failure": {
4629
+ "budget_exhausted": "Dieser Workspace hat sein Modellbudget aufgebraucht.",
4630
+ "model_unavailable": "Für das geführte Review ist kein Modell verfügbar.",
4631
+ "repo_unavailable": "Der Pull Request konnte nicht aus dem Repository gelesen werden.",
4632
+ "generation_failed": "Der Modellaufruf ist fehlgeschlagen.",
4633
+ "unreadable_reply": "Das Modell hat in einer nicht lesbaren Form geantwortet.",
4634
+ "depth_unavailable": "Tiefe Antworten mit einem Checkout sind noch nicht verfügbar.",
4635
+ "head_moved": "Der Pull Request ist über den geprüften Commit hinaus. Aktualisiere, um den neuesten Commit zu prüfen.",
4636
+ "detail": "Details"
4637
+ },
4638
+ "dropped": {
4639
+ "outside_diff": "die Zeile liegt außerhalb des Diffs",
4640
+ "not_in_pr": "die Datei gehört nicht zu diesem Pull Request",
4641
+ "incomplete": "der Vorschlag war unvollständig"
4642
+ },
4643
+ "thread": {
4644
+ "new": "Neuer Thread",
4645
+ "untitled": "Thread ohne Titel",
4646
+ "empty": "Frag alles zu diesem Pull Request.",
4647
+ "waiting": "Antwort wird erstellt…",
4648
+ "placeholder": "Stell eine Frage zur Änderung",
4649
+ "busy": "Warte auf die Antwort auf die vorherige Frage",
4650
+ "send": "Fragen",
4651
+ "draftRequest": "Review-Kommentare angefordert"
4652
+ },
4653
+ "drafts": {
4654
+ "title": "Kommentarentwürfe",
4655
+ "request": "Review-Kommentare entwerfen",
4656
+ "summary": "{kept} von {proposed} vorgeschlagenen Kommentaren als Entwurf behalten.",
4657
+ "side": {
4658
+ "RIGHT": "neuer Code",
4659
+ "LEFT": "alter Code"
4660
+ }
4661
+ },
4662
+ "errors": {
4663
+ "load": "Das Review konnte nicht geladen werden",
4664
+ "repos": "Die verknüpften Repositories konnten nicht geladen werden",
4665
+ "open": "Das Review konnte nicht geöffnet werden",
4666
+ "ask": "Die Frage konnte nicht gestellt werden",
4667
+ "drafts": "Kommentarentwürfe konnten nicht angefordert werden",
4668
+ "refresh": "Das Review konnte nicht aktualisiert werden"
4669
+ }
4670
+ },
4594
4671
  "pipeline": {
4595
4672
  "iterationCap": {
4596
4673
  "extraRound": "Noch eine Runde",
@@ -6173,6 +6250,7 @@
6173
6250
  "commandBar": "Suchen oder einen Befehl ausführen…",
6174
6251
  "create": "Erstellen",
6175
6252
  "assistant": "Assistent",
6253
+ "guidedReview": "Geführtes Review",
6176
6254
  "buildPipeline": "Eine Pipeline erstellen",
6177
6255
  "repositories": "Repositories",
6178
6256
  "addFromRepo": "Aus vorhandenem Repo hinzufügen",
@@ -6313,7 +6391,8 @@
6313
6391
  "no_host_frame": "Diese Expedition liegt unter keinem Service",
6314
6392
  "delegated_executor_unwired": "Externer Executor nicht registriert",
6315
6393
  "delegated_step_async_only": "Dieser Schritt wird abgefragt, nicht abgewartet",
6316
- "delegated_claim_missing": "Für diesen Schritt gibt es keine externe Arbeit zu prüfen"
6394
+ "delegated_claim_missing": "Für diesen Schritt gibt es keine externe Arbeit zu prüfen",
6395
+ "thread_busy": "Dieser Thread antwortet noch"
6317
6396
  },
6318
6397
  "description": {
6319
6398
  "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.",
@@ -6370,7 +6449,8 @@
6370
6449
  "no_host_frame": "Eine Behebungsaufgabe braucht einen Service als Zuhause und ein Repository zum Beheben. Verschieben Sie die Expeditionsaufgabe unter einen Service-Rahmen und versuchen Sie es erneut.",
6371
6450
  "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.",
6372
6451
  "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.",
6373
- "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."
6452
+ "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."
6374
6454
  },
6375
6455
  "action": {
6376
6456
  "connectGitHub": "GitHub verbinden",