@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.
- package/app/components/guidedReview/GuidedReview.logic.spec.ts +123 -0
- package/app/components/guidedReview/GuidedReview.logic.ts +108 -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 +177 -0
- package/app/components/panels/inspector/TaskReviewTarget.vue +36 -0
- package/app/composables/api/guidedReview.ts +80 -0
- package/app/composables/useApi.ts +2 -0
- package/app/composables/useNavContributions.ts +1 -0
- package/app/composables/usePipelineErrorToast.ts +4 -0
- package/app/composables/useWorkspaceStream.ts +4 -0
- package/app/composables/workspaceStream/applyWorkspaceEvent.spec.ts +2 -0
- package/app/composables/workspaceStream/applyWorkspaceEvent.ts +6 -0
- package/app/modular/nav-contributions.ts +20 -0
- package/app/pages/index.vue +4 -0
- package/app/stores/guidedReview.spec.ts +148 -0
- package/app/stores/guidedReview.ts +200 -0
- package/app/stores/ui/modals.ts +25 -1
- package/app/stores/workspace/hydrate.ts +2 -0
- package/app/types/domain.ts +1 -0
- package/app/types/guided-review.ts +16 -0
- package/i18n/locales/de.json +82 -2
- package/i18n/locales/en.json +82 -2
- package/i18n/locales/es.json +82 -2
- package/i18n/locales/fr.json +82 -2
- package/i18n/locales/he.json +82 -2
- package/i18n/locales/it.json +82 -2
- package/i18n/locales/ja.json +82 -2
- package/i18n/locales/pl.json +82 -2
- package/i18n/locales/tr.json +82 -2
- package/i18n/locales/uk.json +82 -2
- 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
|
+
})
|
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,
|
|
@@ -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.
|
package/app/types/domain.ts
CHANGED
|
@@ -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'
|
package/i18n/locales/de.json
CHANGED
|
@@ -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",
|