@cat-factory/app 0.280.1 → 0.281.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/binaryCandidates/BinaryCandidatesWindow.vue +142 -5
- package/app/components/board/AddTaskModal.vue +9 -0
- package/app/components/board/ReviewFrictionDialog.vue +35 -4
- package/app/components/brainstorm/BrainstormWindow.vue +19 -1
- package/app/components/common/ConfirmDialog.vue +26 -0
- package/app/components/docs/DocInterviewWindow.vue +40 -38
- package/app/components/followUp/FollowUpWindow.vue +32 -2
- package/app/components/forkDecision/ForkDecisionWindow.vue +44 -5
- package/app/components/gates/GateResultView.vue +14 -1
- package/app/components/humanTest/HumanTestWindow.vue +13 -1
- package/app/components/initiative/InitiativePlanDecision.vue +16 -3
- package/app/components/initiative/InitiativePlanReview.vue +16 -1
- package/app/components/initiative/InitiativePlanningWindow.vue +50 -52
- package/app/components/initiative/InitiativeTrackerWindow.vue +59 -1
- package/app/components/judge/JudgeResultView.vue +14 -1
- package/app/components/panels/ResultWindowDrafts.logic.spec.ts +233 -0
- package/app/components/panels/inspector/ServiceTestSecrets.vue +17 -6
- package/app/components/pipeline/PipelineHealthModal.vue +55 -16
- package/app/components/prReview/PrReviewWindow.vue +23 -4
- package/app/components/settings/TaskSourceCard.vue +2 -0
- package/app/components/visualConfirm/VisualConfirmationWindow.vue +19 -1
- package/app/composables/useConfirm.spec.ts +62 -0
- package/app/composables/useConfirm.ts +6 -1
- package/app/composables/useInterviewDrafts.spec.ts +198 -0
- package/app/composables/useInterviewDrafts.ts +184 -0
- package/app/composables/usePipelineErrorToast.ts +8 -0
- package/app/stores/binaryCandidates.ts +26 -3
- package/app/stores/kaizen.spec.ts +3 -0
- package/app/stores/ui/modals.ts +11 -0
- package/app/utils/binaryCandidates.spec.ts +45 -1
- package/app/utils/binaryCandidates.ts +33 -0
- package/i18n/locales/de.json +24 -4
- package/i18n/locales/en.json +24 -4
- package/i18n/locales/es.json +24 -4
- package/i18n/locales/fr.json +24 -4
- package/i18n/locales/he.json +24 -4
- package/i18n/locales/it.json +24 -4
- package/i18n/locales/ja.json +24 -4
- package/i18n/locales/pl.json +24 -4
- package/i18n/locales/tr.json +24 -4
- package/i18n/locales/uk.json +24 -4
- package/package.json +2 -2
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { computed, reactive, watch } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-question answer drafts for an INTERVIEW gate window (the initiative planner's interviewer and
|
|
5
|
+
* the doc-authoring interviewer), plus the two ways they leave the browser: one answer on blur, and
|
|
6
|
+
* every dirty answer on the way out.
|
|
7
|
+
*
|
|
8
|
+
* Both windows hold the same shape of draft for the same reason, and recording one is a PLAIN SAVE:
|
|
9
|
+
* it writes the reply without resolving the interview, which is what the window's own two commands
|
|
10
|
+
* do. That is what makes the FLUSH disposition correct here rather than a discard prompt (see
|
|
11
|
+
* `ResultWindowDrafts.logic.spec.ts` for the rule and the per-window table).
|
|
12
|
+
*
|
|
13
|
+
* It is ONE seam because the two windows held byte-identical copies of this logic and only ever got
|
|
14
|
+
* fixed one at a time: the doc interview grew a close-time flush while the planner kept dropping
|
|
15
|
+
* answers on close, and neither reported a failed write. Three properties are easy to lose when this
|
|
16
|
+
* is hand-rolled per window, and each one was:
|
|
17
|
+
*
|
|
18
|
+
* - The flush CAPTURES what it needs synchronously. `blockId` and everything derived from it go
|
|
19
|
+
* null the instant the view tears down, so an awaited loop that re-reads them writes nowhere.
|
|
20
|
+
* - Each answer settles INDEPENDENTLY. A loop that awaited straight through dropped every answer
|
|
21
|
+
* after the first rejection, with the window already gone.
|
|
22
|
+
* - Every path REPORTS its own failure. Both backing stores rethrow and Vue discards a handler's
|
|
23
|
+
* returned promise, so an unreported write is an unhandled rejection and, to the user, a no-op.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** The minimum an exchange has to carry for a draft answer to be held against it. */
|
|
27
|
+
export interface InterviewDraftQuestion {
|
|
28
|
+
/**
|
|
29
|
+
* The id the answer write addresses. Optional because the wire shape leaves it optional (a
|
|
30
|
+
* hand-authored or fixture exchange parses without one); see {@link useInterviewDrafts}'s
|
|
31
|
+
* `addressable`.
|
|
32
|
+
*/
|
|
33
|
+
id?: string
|
|
34
|
+
/** Stable key for the list and the draft map: the question id, or its index as a fallback. */
|
|
35
|
+
key: string
|
|
36
|
+
/** What is already recorded. Seeds the draft, and decides whether the draft is dirty. */
|
|
37
|
+
answer?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useInterviewDrafts<Q extends InterviewDraftQuestion>(opts: {
|
|
41
|
+
/** The block the answers are written against, or null while no view is open. */
|
|
42
|
+
blockId: () => string | null
|
|
43
|
+
/** Every exchange the session holds, settled rounds included. */
|
|
44
|
+
questions: () => Q[]
|
|
45
|
+
/** The subset still owing an answer, which is what the submit button waits on. */
|
|
46
|
+
pending: () => Q[]
|
|
47
|
+
/** Record ONE answer against the block. */
|
|
48
|
+
write: (blockId: string, questionId: string, answer: string) => Promise<unknown>
|
|
49
|
+
/**
|
|
50
|
+
* Whether this question's draft may be written at all. A question set aside as not-relevant had
|
|
51
|
+
* its recorded answer cleared, so writing a stale local draft back would silently re-answer it.
|
|
52
|
+
*/
|
|
53
|
+
writable?: (question: Q) => boolean
|
|
54
|
+
/** Toast titles for a failed write: one answer lost, and several (which takes a `{ count }`). */
|
|
55
|
+
failureTitleKeys: { one: string; many: string }
|
|
56
|
+
}) {
|
|
57
|
+
const { present } = usePipelineErrorToast()
|
|
58
|
+
|
|
59
|
+
const drafts = reactive<Record<string, string>>({})
|
|
60
|
+
// Seeded from the entity and refreshed as new rounds arrive, without clobbering an answer the
|
|
61
|
+
// human is mid-edit on.
|
|
62
|
+
watch(
|
|
63
|
+
opts.questions,
|
|
64
|
+
(list) => {
|
|
65
|
+
for (const question of list) {
|
|
66
|
+
if (!(question.key in drafts)) drafts[question.key] = question.answer ?? ''
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{ immediate: true },
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Whether an answer to this exchange can be RECORDED at all: the write addresses a question BY
|
|
74
|
+
* ID, so one without an id has nowhere for an answer to go.
|
|
75
|
+
*
|
|
76
|
+
* Callers disable that question's input and say why. Accepting text into it instead would take an
|
|
77
|
+
* answer the flush could only drop, which is the silent loss this seam exists to end.
|
|
78
|
+
*/
|
|
79
|
+
function addressable(question: Q): boolean {
|
|
80
|
+
return typeof question.id === 'string' && question.id.length > 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Questions still missing a drafted answer, which is what a submit button gates on (and renders,
|
|
85
|
+
* because a disabled button with no stated reason is itself a "nothing happened").
|
|
86
|
+
*
|
|
87
|
+
* Unaddressable questions are excluded: nothing the human can type clears one, so counting it
|
|
88
|
+
* would disable the submit for good.
|
|
89
|
+
*/
|
|
90
|
+
const unanswered = computed(
|
|
91
|
+
() => opts.pending().filter((q) => addressable(q) && !drafts[q.key]?.trim()).length,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Persist one answer when its draft differs from what is recorded. The block id is threaded in
|
|
96
|
+
* rather than read off `opts.blockId()`, so a flush that started as the window closed still writes
|
|
97
|
+
* to the right board.
|
|
98
|
+
*/
|
|
99
|
+
async function persist(blockId: string, question: Q): Promise<void> {
|
|
100
|
+
const id = question.id
|
|
101
|
+
if (!id || opts.writable?.(question) === false) return
|
|
102
|
+
const next = (drafts[question.key] ?? '').trim()
|
|
103
|
+
if (!next || next === (question.answer ?? '').trim()) return
|
|
104
|
+
await opts.write(blockId, id, next)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Report a failed write, naming how many answers did not make it. */
|
|
108
|
+
function report(failed: number, cause: unknown): void {
|
|
109
|
+
present(cause, failed === 1 ? opts.failureTitleKeys.one : opts.failureTitleKeys.many, {
|
|
110
|
+
count: failed,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Persist every dirty draft, each on its OWN: one rejection may not cost the answers after it.
|
|
116
|
+
* Returns how many could not be written plus the first cause, which is what the caller reports.
|
|
117
|
+
*
|
|
118
|
+
* Sequential rather than concurrent because each write is a read-modify-write of the session's one
|
|
119
|
+
* question-and-answer array, so two in flight would race to overwrite each other's answer.
|
|
120
|
+
*/
|
|
121
|
+
async function flushAll(blockId: string, list: Q[]): Promise<{ failed: number; cause: unknown }> {
|
|
122
|
+
let failed = 0
|
|
123
|
+
let cause: unknown
|
|
124
|
+
for (const question of list) {
|
|
125
|
+
try {
|
|
126
|
+
await persist(blockId, question)
|
|
127
|
+
} catch (error) {
|
|
128
|
+
failed += 1
|
|
129
|
+
if (failed === 1) cause = error
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { failed, cause }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Save this ONE answer against the block that is open right now: the blur handler, and the same
|
|
137
|
+
* call an adopted recommendation makes. Detached, because Vue discards a handler's returned
|
|
138
|
+
* promise, so the failure is reported here or nowhere.
|
|
139
|
+
*/
|
|
140
|
+
function saveAnswer(question: Q): void {
|
|
141
|
+
const blockId = opts.blockId()
|
|
142
|
+
if (!blockId) return
|
|
143
|
+
void persist(blockId, question).catch((error) => report(1, error))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Persist every dirty draft on the way out, detached so it can be called from the synchronous
|
|
148
|
+
* close hook. A close-time flush is the one path with no button left on screen to have reported a
|
|
149
|
+
* failure, so it reports the loss itself.
|
|
150
|
+
*/
|
|
151
|
+
function flushDrafts(): void {
|
|
152
|
+
const blockId = opts.blockId()
|
|
153
|
+
if (!blockId) return
|
|
154
|
+
const list = [...opts.questions()]
|
|
155
|
+
void (async () => {
|
|
156
|
+
const { failed, cause } = await flushAll(blockId, list)
|
|
157
|
+
if (failed > 0) report(failed, cause)
|
|
158
|
+
})()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Flush every dirty draft, then run a window action, but ONLY if every draft was written: an
|
|
163
|
+
* answer that failed to save may not be submitted as if it were there. On a failure the report is
|
|
164
|
+
* the whole outcome, and the window is left as it is, with the same button on screen and the text
|
|
165
|
+
* still in its box.
|
|
166
|
+
*/
|
|
167
|
+
async function flushThen(
|
|
168
|
+
action: (blockId: string) => Promise<unknown>,
|
|
169
|
+
failureTitleKey: string,
|
|
170
|
+
): Promise<void> {
|
|
171
|
+
const blockId = opts.blockId()
|
|
172
|
+
if (!blockId) return
|
|
173
|
+
const { failed, cause } = await flushAll(blockId, [...opts.questions()])
|
|
174
|
+
if (failed > 0) {
|
|
175
|
+
report(failed, cause)
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
// The action rejects too (both backing stores rethrow) and it is reached from a click handler
|
|
179
|
+
// whose promise Vue discards, so its failure is reported here or nowhere.
|
|
180
|
+
await action(blockId).catch((error) => present(error, failureTitleKey))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { drafts, addressable, unanswered, saveAnswer, flushDrafts, flushThen }
|
|
184
|
+
}
|
|
@@ -102,6 +102,14 @@ const CONFLICT_INFO: Record<Exclude<ConflictReason, BespokeConflictReason>, Conf
|
|
|
102
102
|
titleKey: 'errors.conflict.title.risk_policy_not_inherited',
|
|
103
103
|
descriptionKey: 'errors.conflict.description.risk_policy_not_inherited',
|
|
104
104
|
},
|
|
105
|
+
// Raised only by the public `/api/v1/kaizen/entries/:id/acknowledge` route today, so no SPA
|
|
106
|
+
// action reaches it. Mapped all the same, because the map is exhaustive over the wire
|
|
107
|
+
// vocabulary rather than over the subset this app happens to trigger: the day a Kaizen screen
|
|
108
|
+
// grows an acknowledge button, the copy is already here rather than an untranslated fallback.
|
|
109
|
+
kaizen_entry_not_settled: {
|
|
110
|
+
titleKey: 'errors.conflict.title.kaizen_entry_not_settled',
|
|
111
|
+
descriptionKey: 'errors.conflict.description.kaizen_entry_not_settled',
|
|
112
|
+
},
|
|
105
113
|
task_limit_reached: {
|
|
106
114
|
titleKey: 'errors.conflict.title.task_limit_reached',
|
|
107
115
|
descriptionKey: 'errors.conflict.description.task_limit_reached',
|
|
@@ -21,6 +21,12 @@ export const useBinaryCandidatesStore = defineStore('binaryCandidates', () => {
|
|
|
21
21
|
|
|
22
22
|
/** True while a keep call is in flight (drives the button spinner / disabled state). */
|
|
23
23
|
const keeping = ref(false)
|
|
24
|
+
/**
|
|
25
|
+
* True while the warm-up read is in flight. The window renders no state until it settles, and
|
|
26
|
+
* "still fetching" must not render as the "nothing to choose between" empty state: on this
|
|
27
|
+
* surface that empty state is a claim the run generated nothing to compare.
|
|
28
|
+
*/
|
|
29
|
+
const loading = ref(false)
|
|
24
30
|
/** The last error message from an action, surfaced inline; cleared on the next action. */
|
|
25
31
|
const error = ref<string | null>(null)
|
|
26
32
|
|
|
@@ -48,9 +54,24 @@ export const useBinaryCandidatesStore = defineStore('binaryCandidates', () => {
|
|
|
48
54
|
if (step) step.binaryCandidates = state
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* The most recent {@link load} attempt. Only the latest one may write `loading` or `error`: two
|
|
59
|
+
* reads do overlap (a Retry beside a window switching run, an open triggered while one is in
|
|
60
|
+
* flight), and a superseded attempt settling afterwards used to clear the spinner and stamp its
|
|
61
|
+
* own verdict over the newer read's. A slow failure landing after a fast success is the bad case,
|
|
62
|
+
* because it reports a load failure across candidates that are on screen.
|
|
63
|
+
*/
|
|
64
|
+
let attempt = 0
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Warm the live state from the GET (the stream also keeps it fresh). The failure is RECORDED
|
|
68
|
+
* rather than swallowed: with no state on the step, a failed read and a run that produced no
|
|
69
|
+
* candidates are the same `null` and opposite facts, and only one of them is worth a Retry.
|
|
70
|
+
*/
|
|
52
71
|
async function load(executionId: string): Promise<void> {
|
|
72
|
+
const mine = ++attempt
|
|
53
73
|
error.value = null
|
|
74
|
+
loading.value = true
|
|
54
75
|
try {
|
|
55
76
|
await execution.echoAfter(
|
|
56
77
|
executionId,
|
|
@@ -60,7 +81,9 @@ export const useBinaryCandidatesStore = defineStore('binaryCandidates', () => {
|
|
|
60
81
|
},
|
|
61
82
|
)
|
|
62
83
|
} catch (e) {
|
|
63
|
-
error.value = e instanceof Error ? e.message : 'Failed to load'
|
|
84
|
+
if (mine === attempt) error.value = e instanceof Error ? e.message : 'Failed to load'
|
|
85
|
+
} finally {
|
|
86
|
+
if (mine === attempt) loading.value = false
|
|
64
87
|
}
|
|
65
88
|
}
|
|
66
89
|
|
|
@@ -85,5 +108,5 @@ export const useBinaryCandidatesStore = defineStore('binaryCandidates', () => {
|
|
|
85
108
|
}
|
|
86
109
|
}
|
|
87
110
|
|
|
88
|
-
return { keeping, error, load, keep }
|
|
111
|
+
return { keeping, loading, error, load, keep }
|
|
89
112
|
})
|
package/app/stores/ui/modals.ts
CHANGED
|
@@ -74,6 +74,17 @@ export interface ReviewFrictionModalContext {
|
|
|
74
74
|
debt: ReviewDebtRow[]
|
|
75
75
|
/** Retry the create with `acknowledgeReviewDebt` — present only for the soft `warn` tier. */
|
|
76
76
|
onConfirm: (() => void) | null
|
|
77
|
+
/**
|
|
78
|
+
* Whether the opener's create is in flight right now, so "Create anyway" can show its spinner and
|
|
79
|
+
* refuse a second click (UX-78). A GETTER over the opener's own `saving` ref rather than a copied
|
|
80
|
+
* boolean: the dialog's context object is captured once at open, so a snapshot would be frozen at
|
|
81
|
+
* `false` for the whole retry. Reading it inside a `computed` keeps the reactivity.
|
|
82
|
+
*
|
|
83
|
+
* Load-bearing, not cosmetic: the retry's first await resolves the staged context attachments
|
|
84
|
+
* (a network round-trip per item), so a second click during that window filed a SECOND task and
|
|
85
|
+
* started a second pipeline run against the same request.
|
|
86
|
+
*/
|
|
87
|
+
pending?: () => boolean
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
/** Clears both hub came-from markers; injected into the slices whose `open*` handlers reset them. */
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import type { PipelineStep } from '~/types/execution'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
binaryCandidateAbsence,
|
|
5
|
+
binaryCandidateHasWarnings,
|
|
6
|
+
binaryCandidateView,
|
|
7
|
+
} from './binaryCandidates'
|
|
4
8
|
|
|
5
9
|
function step(overrides: Record<string, unknown> = {}): PipelineStep {
|
|
6
10
|
// The candidate override MERGES into the base state rather than replacing it, so a case that
|
|
@@ -108,3 +112,43 @@ describe('binaryCandidateHasWarnings', () => {
|
|
|
108
112
|
}
|
|
109
113
|
})
|
|
110
114
|
})
|
|
115
|
+
|
|
116
|
+
describe('binaryCandidateAbsence', () => {
|
|
117
|
+
const RUN = 'exec_1'
|
|
118
|
+
|
|
119
|
+
// The window used to render a titled shell with a blank body for all four of these (UX-80).
|
|
120
|
+
// They are different facts, and only one of them is a claim about the RUN.
|
|
121
|
+
it('separates no run, a read in flight, a failed read, and a run that compared nothing', () => {
|
|
122
|
+
expect(binaryCandidateAbsence(false, null, null)).toBe('no_run')
|
|
123
|
+
expect(binaryCandidateAbsence(true, null, RUN)).toBe('loading')
|
|
124
|
+
expect(binaryCandidateAbsence(false, 'network down', RUN)).toBe('load_failed')
|
|
125
|
+
expect(binaryCandidateAbsence(false, null, RUN)).toBe('nothing_compared')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// A Retry re-enters `loading` while the PREVIOUS attempt's message is still recorded. Reporting
|
|
129
|
+
// the stale failure over the live attempt would make the button look like it did nothing.
|
|
130
|
+
it('lets a fresh attempt outrank the error it is clearing', () => {
|
|
131
|
+
expect(binaryCandidateAbsence(true, 'network down', RUN)).toBe('loading')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// 'nothing_compared' renders "nothing to compare", which is a statement about what the run
|
|
135
|
+
// produced. A request that never landed knows nothing about that, so it must never reach here.
|
|
136
|
+
it('never claims emptiness on the strength of a request that failed', () => {
|
|
137
|
+
for (const error of ['boom', 'Failed to load']) {
|
|
138
|
+
expect(binaryCandidateAbsence(false, error, RUN)).not.toBe('nothing_compared')
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// With no run the warm-up read is never started, so the store's flags belong to some OTHER
|
|
143
|
+
// window's read. Reporting them here would describe a run nobody asked about, and falling through
|
|
144
|
+
// to emptiness would claim this run compared nothing without ever having looked.
|
|
145
|
+
it('never reports a state it did not read, nor emptiness, when there is no run', () => {
|
|
146
|
+
for (const loading of [true, false]) {
|
|
147
|
+
for (const error of [null, 'network down']) {
|
|
148
|
+
for (const run of [null, undefined, '']) {
|
|
149
|
+
expect(binaryCandidateAbsence(loading, error, run)).toBe('no_run')
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
})
|
|
@@ -127,6 +127,39 @@ export function binaryCandidateHasWarnings(view: BinaryCandidateView): boolean {
|
|
|
127
127
|
return invalidEntries > 0 || omitted > 0 || unusablePreviews > 0
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Why the window has no comparison to render. Four different facts that a single blank body
|
|
132
|
+
* conflated (UX-80), each needing a different reaction from the reader: nothing to read from, wait,
|
|
133
|
+
* retry, or accept that the run compared nothing. `nothing_compared` is a CLAIM about the run, so
|
|
134
|
+
* it is only ever the answer once a read of that run has settled and not failed.
|
|
135
|
+
*/
|
|
136
|
+
export type BinaryCandidateAbsence = 'no_run' | 'loading' | 'load_failed' | 'nothing_compared'
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The absence to render when {@link binaryCandidateView} produced nothing.
|
|
140
|
+
*
|
|
141
|
+
* Precedence is the point, and it is stated here rather than in the template's branch order:
|
|
142
|
+
*
|
|
143
|
+
* - No RUN outranks everything. The window is keyed to an execution and the warm-up read is
|
|
144
|
+
* skipped without one, so `loading`/`error` can only be describing some other window's read;
|
|
145
|
+
* reporting either would attribute a state to a run nobody asked about, and reporting emptiness
|
|
146
|
+
* would claim this run compared nothing on the strength of never having looked.
|
|
147
|
+
* - An in-flight read outranks a stale error from the previous attempt, so a Retry does not keep
|
|
148
|
+
* showing the failure it is busy clearing.
|
|
149
|
+
* - A recorded error outranks emptiness, so a request that never landed cannot render as "this run
|
|
150
|
+
* generated nothing to compare".
|
|
151
|
+
*/
|
|
152
|
+
export function binaryCandidateAbsence(
|
|
153
|
+
loading: boolean,
|
|
154
|
+
error: string | null | undefined,
|
|
155
|
+
executionId: string | null | undefined,
|
|
156
|
+
): BinaryCandidateAbsence {
|
|
157
|
+
if (!executionId) return 'no_run'
|
|
158
|
+
if (loading) return 'loading'
|
|
159
|
+
if (error) return 'load_failed'
|
|
160
|
+
return 'nothing_compared'
|
|
161
|
+
}
|
|
162
|
+
|
|
130
163
|
/**
|
|
131
164
|
* State → the i18n key for the line explaining why no choice was offered.
|
|
132
165
|
*
|
package/i18n/locales/de.json
CHANGED
|
@@ -5689,6 +5689,11 @@
|
|
|
5689
5689
|
"preparingHint": "Die Planung liest zuerst das Repository, damit Sie nicht nach dem gefragt werden, was der Code beantworten kann. Fragen erscheinen hier, sobald das erledigt ist.",
|
|
5690
5690
|
"failed": "Der Planungslauf wurde abgebrochen",
|
|
5691
5691
|
"failedHint": "Er endete, bevor der Planer antworten konnte. Ihre Antworten sind gespeichert; führen Sie die Planung von der Initiative aus erneut aus.",
|
|
5692
|
+
"saveFailed": "Antwort konnte nicht gespeichert werden",
|
|
5693
|
+
"saveFailedCount": "{count} Ihrer Antworten konnten nicht gespeichert werden",
|
|
5694
|
+
"continueFailed": "Antworten konnten nicht gesendet werden",
|
|
5695
|
+
"proceedFailed": "Die Planung konnte nicht gestartet werden",
|
|
5696
|
+
"unanswerable": "Diese Frage hat keine ID, daher kann keine Antwort dazu gespeichert werden.",
|
|
5692
5697
|
"answerPlaceholder": "Ihre Antwort",
|
|
5693
5698
|
"hint": "Antworten senden lässt den Planer Rückfragen stellen; Jetzt planen entwirft den Plan mit den bisherigen Antworten.",
|
|
5694
5699
|
"unanswered": "Unbeantwortete Fragen: {count}",
|
|
@@ -5966,7 +5971,8 @@
|
|
|
5966
5971
|
"submission_not_allowed": "Zusammenführen für diesen Lauf nicht erlaubt",
|
|
5967
5972
|
"webhook_limit_reached": "Webhook-Limit erreicht",
|
|
5968
5973
|
"risk_policy_inherited": "Diese Richtlinie gehört zum Konto",
|
|
5969
|
-
"risk_policy_not_inherited": "Diese Richtlinie gehört zu diesem Board"
|
|
5974
|
+
"risk_policy_not_inherited": "Diese Richtlinie gehört zu diesem Board",
|
|
5975
|
+
"kaizen_entry_not_settled": "Der Kaizen-Eintrag wurde noch nicht bewertet"
|
|
5970
5976
|
},
|
|
5971
5977
|
"description": {
|
|
5972
5978
|
"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.",
|
|
@@ -6007,7 +6013,8 @@
|
|
|
6007
6013
|
"submission_not_allowed": "Die Merge-Richtlinie dieser Aufgabe erlaubt der Rolle, die diesen Lauf gestartet hat, diese Änderungsart nicht zusammenzuführen. Jemand mit einer passenden Rolle kann es tun, oder ein Admin erweitert die Richtlinie in der Merge-Vorlage.",
|
|
6008
6014
|
"webhook_limit_reached": "Für diesen Workspace ist bereits die maximale Anzahl ausgehender Webhooks registriert. Entfernen Sie einen nicht mehr benötigten und registrieren Sie diesen erneut.",
|
|
6009
6015
|
"risk_policy_inherited": "Sie gilt für alle Boards des Kontos und kann daher nicht von hier geändert werden. Klone sie in dieses Board und bearbeite die Kopie.",
|
|
6010
|
-
"risk_policy_not_inherited": "Sie ist bereits die eigene Richtlinie dieses Boards: bearbeite sie direkt oder lösche sie, wenn sie nicht mehr angeboten werden soll."
|
|
6016
|
+
"risk_policy_not_inherited": "Sie ist bereits die eigene Richtlinie dieses Boards: bearbeite sie direkt oder lösche sie, wenn sie nicht mehr angeboten werden soll.",
|
|
6017
|
+
"kaizen_entry_not_settled": "Die Bewertung steht noch aus oder läuft gerade, es gibt also keine Empfehlungen zum Bestätigen. Versuchen Sie es erneut, sobald sie abgeschlossen ist."
|
|
6011
6018
|
},
|
|
6012
6019
|
"action": {
|
|
6013
6020
|
"connectGitHub": "GitHub verbinden",
|
|
@@ -6194,7 +6201,12 @@
|
|
|
6194
6201
|
"status": {
|
|
6195
6202
|
"awaiting": "Warten auf Antworten",
|
|
6196
6203
|
"done": "Fertig"
|
|
6197
|
-
}
|
|
6204
|
+
},
|
|
6205
|
+
"saveFailedCount": "{count} Ihrer Antworten konnten nicht gespeichert werden",
|
|
6206
|
+
"continueFailed": "Antworten konnten nicht gesendet werden",
|
|
6207
|
+
"proceedFailed": "Der Entwurf konnte nicht gestartet werden",
|
|
6208
|
+
"unanswerable": "Diese Frage hat keine ID, daher kann keine Antwort dazu gespeichert werden.",
|
|
6209
|
+
"saveFailed": "Antwort konnte nicht gespeichert werden"
|
|
6198
6210
|
},
|
|
6199
6211
|
"gates": {
|
|
6200
6212
|
"subtitle": {
|
|
@@ -8035,6 +8047,14 @@
|
|
|
8035
8047
|
"undeclared": "Der Schritt hat seine Kandidaten nie deklariert, es gab also nichts zu vergleichen. Der Lauf ging weiter.",
|
|
8036
8048
|
"parseFailed": "Die Kandidaten-Deklaration des Schritts war nicht lesbar, es gab also nichts zu vergleichen. Der Lauf ging weiter.",
|
|
8037
8049
|
"noCandidates": "Der Schritt hat keine Kandidaten bereitgestellt, es gab also nichts zu vergleichen. Der Lauf ging weiter."
|
|
8038
|
-
}
|
|
8050
|
+
},
|
|
8051
|
+
"noRun": {
|
|
8052
|
+
"title": "Kein Lauf zum Auslesen",
|
|
8053
|
+
"hint": "Dieser Vergleich ist an einen Pipeline-Lauf gebunden. Öffnen Sie ihn über die Karte oder die Zeitleiste des erzeugenden Schritts, damit die Kandidaten geladen werden können."
|
|
8054
|
+
},
|
|
8055
|
+
"empty": {
|
|
8056
|
+
"title": "Nichts zu vergleichen"
|
|
8057
|
+
},
|
|
8058
|
+
"loadFailed": "Die erzeugten Kandidaten konnten nicht geladen werden."
|
|
8039
8059
|
}
|
|
8040
8060
|
}
|
package/i18n/locales/en.json
CHANGED
|
@@ -757,7 +757,8 @@
|
|
|
757
757
|
"submission_not_allowed": "Merge not allowed for this run",
|
|
758
758
|
"webhook_limit_reached": "Webhook limit reached",
|
|
759
759
|
"risk_policy_inherited": "This policy belongs to the account",
|
|
760
|
-
"risk_policy_not_inherited": "This policy belongs to this board"
|
|
760
|
+
"risk_policy_not_inherited": "This policy belongs to this board",
|
|
761
|
+
"kaizen_entry_not_settled": "The Kaizen entry has not been graded yet"
|
|
761
762
|
},
|
|
762
763
|
"description": {
|
|
763
764
|
"dependencies_unmet": "This task depends on others that aren't finished yet. Complete or unblock them, then start it again.",
|
|
@@ -801,7 +802,8 @@
|
|
|
801
802
|
"submission_not_allowed": "This task's merge policy doesn't let the role that started this run merge this kind of change. Somebody whose role may merge it can do so, or an admin can widen the policy in the merge preset.",
|
|
802
803
|
"webhook_limit_reached": "This workspace already has the maximum number of outbound webhooks registered. Remove one you no longer need, then register this again.",
|
|
803
804
|
"risk_policy_inherited": "It is shared with every board in the account, so it cannot be changed from here. Clone it to this board and edit the copy.",
|
|
804
|
-
"risk_policy_not_inherited": "It is already this board's own policy: edit it directly, or delete it if you no longer want it offered."
|
|
805
|
+
"risk_policy_not_inherited": "It is already this board's own policy: edit it directly, or delete it if you no longer want it offered.",
|
|
806
|
+
"kaizen_entry_not_settled": "Its grading is still queued or running, so there are no recommendations to acknowledge. Try again once it finishes."
|
|
805
807
|
},
|
|
806
808
|
"action": {
|
|
807
809
|
"connectGitHub": "Connect GitHub",
|
|
@@ -4768,7 +4770,12 @@
|
|
|
4768
4770
|
"status": {
|
|
4769
4771
|
"awaiting": "Awaiting answers",
|
|
4770
4772
|
"done": "Done"
|
|
4771
|
-
}
|
|
4773
|
+
},
|
|
4774
|
+
"saveFailedCount": "Could not save {count} of your answers",
|
|
4775
|
+
"continueFailed": "Could not submit your answers",
|
|
4776
|
+
"proceedFailed": "Could not start the draft",
|
|
4777
|
+
"unanswerable": "This question carries no id, so an answer cannot be recorded against it.",
|
|
4778
|
+
"saveFailed": "Could not save your answer"
|
|
4772
4779
|
},
|
|
4773
4780
|
"documents": {
|
|
4774
4781
|
"picker": {
|
|
@@ -7345,6 +7352,11 @@
|
|
|
7345
7352
|
"preparingHint": "Planning reads the repository first, so you are not asked what the code can answer. Questions appear here once that is done.",
|
|
7346
7353
|
"failed": "The planning run stopped",
|
|
7347
7354
|
"failedHint": "It ended before the planner could answer. Your answers are saved; re-run planning from the initiative to try again.",
|
|
7355
|
+
"saveFailed": "Could not save your answer",
|
|
7356
|
+
"saveFailedCount": "Could not save {count} of your answers",
|
|
7357
|
+
"continueFailed": "Could not submit your answers",
|
|
7358
|
+
"proceedFailed": "Could not start planning",
|
|
7359
|
+
"unanswerable": "This question carries no id, so an answer cannot be recorded against it.",
|
|
7348
7360
|
"answerPlaceholder": "Your answer",
|
|
7349
7361
|
"hint": "Submit answers lets the planner ask follow-ups; Plan now drafts the plan with the answers so far.",
|
|
7350
7362
|
"unanswered": "Unanswered questions: {count}",
|
|
@@ -8324,6 +8336,14 @@
|
|
|
8324
8336
|
"undeclared": "The step never declared its candidates, so there was nothing to compare. The run continued.",
|
|
8325
8337
|
"parseFailed": "The step’s candidate declaration could not be read, so there was nothing to compare. The run continued.",
|
|
8326
8338
|
"noCandidates": "The step staged no candidates, so there was nothing to compare. The run continued."
|
|
8327
|
-
}
|
|
8339
|
+
},
|
|
8340
|
+
"noRun": {
|
|
8341
|
+
"title": "No run to read",
|
|
8342
|
+
"hint": "This comparison is keyed to a pipeline run. Open it from the generating step's card or timeline so its candidates can be loaded."
|
|
8343
|
+
},
|
|
8344
|
+
"empty": {
|
|
8345
|
+
"title": "Nothing to compare"
|
|
8346
|
+
},
|
|
8347
|
+
"loadFailed": "Could not load the generated candidates."
|
|
8328
8348
|
}
|
|
8329
8349
|
}
|
package/i18n/locales/es.json
CHANGED
|
@@ -682,7 +682,8 @@
|
|
|
682
682
|
"submission_not_allowed": "Fusión no permitida para esta ejecución",
|
|
683
683
|
"webhook_limit_reached": "Límite de webhooks alcanzado",
|
|
684
684
|
"risk_policy_inherited": "Esta política pertenece a la cuenta",
|
|
685
|
-
"risk_policy_not_inherited": "Esta política pertenece a este tablero"
|
|
685
|
+
"risk_policy_not_inherited": "Esta política pertenece a este tablero",
|
|
686
|
+
"kaizen_entry_not_settled": "La entrada de Kaizen aún no se ha evaluado"
|
|
686
687
|
},
|
|
687
688
|
"description": {
|
|
688
689
|
"dependencies_unmet": "Esta tarea depende de otras que aún no están terminadas. Complétalas o desbloquéalas y vuelve a iniciarla.",
|
|
@@ -723,7 +724,8 @@
|
|
|
723
724
|
"submission_not_allowed": "La política de fusión de esta tarea no permite que el rol que inició la ejecución fusione este tipo de cambio. Alguien cuyo rol sí lo permita puede hacerlo, o un administrador puede ampliar la política en el preajuste de fusión.",
|
|
724
725
|
"webhook_limit_reached": "Este espacio de trabajo ya tiene registrado el número máximo de webhooks salientes. Elimina uno que ya no necesites y vuelve a registrar este.",
|
|
725
726
|
"risk_policy_inherited": "Se comparte con todos los tableros de la cuenta, así que no puede cambiarse desde aquí. Clónala en este tablero y edita la copia.",
|
|
726
|
-
"risk_policy_not_inherited": "Ya es la política propia de este tablero: edítala directamente o elimínala si no quieres que se ofrezca."
|
|
727
|
+
"risk_policy_not_inherited": "Ya es la política propia de este tablero: edítala directamente o elimínala si no quieres que se ofrezca.",
|
|
728
|
+
"kaizen_entry_not_settled": "Su evaluación sigue en cola o en curso, así que no hay recomendaciones que confirmar. Inténtalo de nuevo cuando termine."
|
|
727
729
|
},
|
|
728
730
|
"action": {
|
|
729
731
|
"connectGitHub": "Conectar GitHub",
|
|
@@ -4607,7 +4609,12 @@
|
|
|
4607
4609
|
"status": {
|
|
4608
4610
|
"awaiting": "Esperando respuestas",
|
|
4609
4611
|
"done": "Hecho"
|
|
4610
|
-
}
|
|
4612
|
+
},
|
|
4613
|
+
"saveFailedCount": "No se pudieron guardar {count} de tus respuestas",
|
|
4614
|
+
"continueFailed": "No se pudieron enviar tus respuestas",
|
|
4615
|
+
"proceedFailed": "No se pudo iniciar el borrador",
|
|
4616
|
+
"unanswerable": "Esta pregunta no tiene id, así que no se puede registrar ninguna respuesta para ella.",
|
|
4617
|
+
"saveFailed": "No se pudo guardar tu respuesta"
|
|
4611
4618
|
},
|
|
4612
4619
|
"documents": {
|
|
4613
4620
|
"picker": {
|
|
@@ -7086,6 +7093,11 @@
|
|
|
7086
7093
|
"preparingHint": "La planificacion lee primero el repositorio para no preguntarte lo que el codigo ya responde. Las preguntas apareceran aqui cuando termine.",
|
|
7087
7094
|
"failed": "La ejecucion de planificacion se detuvo",
|
|
7088
7095
|
"failedHint": "Termino antes de que el planificador pudiera responder. Tus respuestas estan guardadas; vuelve a ejecutar la planificacion desde la iniciativa.",
|
|
7096
|
+
"saveFailed": "No se pudo guardar tu respuesta",
|
|
7097
|
+
"saveFailedCount": "No se pudieron guardar {count} de tus respuestas",
|
|
7098
|
+
"continueFailed": "No se pudieron enviar tus respuestas",
|
|
7099
|
+
"proceedFailed": "No se pudo iniciar la planificación",
|
|
7100
|
+
"unanswerable": "Esta pregunta no tiene id, así que no se puede registrar ninguna respuesta para ella.",
|
|
7089
7101
|
"answerPlaceholder": "Tu respuesta",
|
|
7090
7102
|
"hint": "Enviar respuestas permite al planificador hacer mas preguntas; Planificar ahora redacta el plan con las respuestas actuales.",
|
|
7091
7103
|
"unanswered": "Preguntas sin responder: {count}",
|
|
@@ -8035,6 +8047,14 @@
|
|
|
8035
8047
|
"undeclared": "El paso nunca declaró sus candidatos, así que no había nada que comparar. La ejecución continuó.",
|
|
8036
8048
|
"parseFailed": "No se pudo leer la declaración de candidatos del paso, así que no había nada que comparar. La ejecución continuó.",
|
|
8037
8049
|
"noCandidates": "El paso no preparó ningún candidato, así que no había nada que comparar. La ejecución continuó."
|
|
8038
|
-
}
|
|
8050
|
+
},
|
|
8051
|
+
"noRun": {
|
|
8052
|
+
"title": "No hay ninguna ejecución que leer",
|
|
8053
|
+
"hint": "Esta comparación está vinculada a una ejecución de la canalización. Ábrela desde la tarjeta o la cronología del paso que la generó para que se puedan cargar sus candidatos."
|
|
8054
|
+
},
|
|
8055
|
+
"empty": {
|
|
8056
|
+
"title": "Nada que comparar"
|
|
8057
|
+
},
|
|
8058
|
+
"loadFailed": "No se pudieron cargar los candidatos generados."
|
|
8039
8059
|
}
|
|
8040
8060
|
}
|
package/i18n/locales/fr.json
CHANGED
|
@@ -682,7 +682,8 @@
|
|
|
682
682
|
"submission_not_allowed": "Fusion non autorisée pour cette exécution",
|
|
683
683
|
"webhook_limit_reached": "Limite de webhooks atteinte",
|
|
684
684
|
"risk_policy_inherited": "Cette politique appartient au compte",
|
|
685
|
-
"risk_policy_not_inherited": "Cette politique appartient à ce tableau"
|
|
685
|
+
"risk_policy_not_inherited": "Cette politique appartient à ce tableau",
|
|
686
|
+
"kaizen_entry_not_settled": "L'entrée Kaizen n'a pas encore été évaluée"
|
|
686
687
|
},
|
|
687
688
|
"description": {
|
|
688
689
|
"dependencies_unmet": "Cette tâche dépend d'autres qui ne sont pas encore terminées. Terminez-les ou débloquez-les, puis relancez-la.",
|
|
@@ -723,7 +724,8 @@
|
|
|
723
724
|
"submission_not_allowed": "La politique de fusion de cette tâche n'autorise pas le rôle qui a lancé cette exécution à fusionner ce type de changement. Un coéquipier dont le rôle le permet peut le faire, ou un admin peut élargir la politique dans le préréglage de fusion.",
|
|
724
725
|
"webhook_limit_reached": "Cet espace de travail a déjà enregistré le nombre maximal de webhooks sortants. Supprimez-en un dont vous n’avez plus besoin, puis enregistrez celui-ci à nouveau.",
|
|
725
726
|
"risk_policy_inherited": "Elle est partagée par tous les tableaux du compte et ne peut donc pas être modifiée ici. Clonez-la vers ce tableau et modifiez la copie.",
|
|
726
|
-
"risk_policy_not_inherited": "C'est déjà la politique propre à ce tableau : modifiez-la directement, ou supprimez-la si vous ne voulez plus qu'elle soit proposée."
|
|
727
|
+
"risk_policy_not_inherited": "C'est déjà la politique propre à ce tableau : modifiez-la directement, ou supprimez-la si vous ne voulez plus qu'elle soit proposée.",
|
|
728
|
+
"kaizen_entry_not_settled": "Son évaluation est encore en attente ou en cours, il n'y a donc aucune recommandation à accuser réception. Réessayez une fois qu'elle sera terminée."
|
|
727
729
|
},
|
|
728
730
|
"action": {
|
|
729
731
|
"connectGitHub": "Connecter GitHub",
|
|
@@ -4607,7 +4609,12 @@
|
|
|
4607
4609
|
"status": {
|
|
4608
4610
|
"awaiting": "En attente de réponses",
|
|
4609
4611
|
"done": "Terminé"
|
|
4610
|
-
}
|
|
4612
|
+
},
|
|
4613
|
+
"saveFailedCount": "Impossible d’enregistrer {count} de vos réponses",
|
|
4614
|
+
"continueFailed": "Impossible d’envoyer vos réponses",
|
|
4615
|
+
"proceedFailed": "Impossible de lancer la rédaction",
|
|
4616
|
+
"unanswerable": "Cette question n’a pas d’identifiant : aucune réponse ne peut y être enregistrée.",
|
|
4617
|
+
"saveFailed": "Impossible d’enregistrer votre réponse"
|
|
4611
4618
|
},
|
|
4612
4619
|
"documents": {
|
|
4613
4620
|
"picker": {
|
|
@@ -7086,6 +7093,11 @@
|
|
|
7086
7093
|
"preparingHint": "La planification lit d'abord le depot afin de ne pas vous demander ce que le code peut repondre. Les questions apparaitront ici une fois termine.",
|
|
7087
7094
|
"failed": "L'execution de planification s'est arretee",
|
|
7088
7095
|
"failedHint": "Elle s'est terminee avant que le planificateur puisse repondre. Vos reponses sont enregistrees ; relancez la planification depuis l'initiative.",
|
|
7096
|
+
"saveFailed": "Impossible d’enregistrer votre réponse",
|
|
7097
|
+
"saveFailedCount": "Impossible d’enregistrer {count} de vos réponses",
|
|
7098
|
+
"continueFailed": "Impossible d’envoyer vos réponses",
|
|
7099
|
+
"proceedFailed": "Impossible de lancer la planification",
|
|
7100
|
+
"unanswerable": "Cette question n’a pas d’identifiant : aucune réponse ne peut y être enregistrée.",
|
|
7089
7101
|
"answerPlaceholder": "Votre reponse",
|
|
7090
7102
|
"hint": "Envoyer les reponses permet au planificateur de poser des questions complementaires ; Planifier maintenant redige le plan avec les reponses actuelles.",
|
|
7091
7103
|
"unanswered": "Questions sans reponse : {count}",
|
|
@@ -8035,6 +8047,14 @@
|
|
|
8035
8047
|
"undeclared": "L’étape n’a jamais déclaré ses candidats, il n’y avait donc rien à comparer. L’exécution a continué.",
|
|
8036
8048
|
"parseFailed": "La déclaration de candidats de l’étape était illisible, il n’y avait donc rien à comparer. L’exécution a continué.",
|
|
8037
8049
|
"noCandidates": "L’étape n’a préparé aucun candidat, il n’y avait donc rien à comparer. L’exécution a continué."
|
|
8038
|
-
}
|
|
8050
|
+
},
|
|
8051
|
+
"noRun": {
|
|
8052
|
+
"title": "Aucune exécution à lire",
|
|
8053
|
+
"hint": "Cette comparaison est liée à une exécution de pipeline. Ouvrez-la depuis la carte ou la chronologie de l’étape qui l’a produite pour que ses candidats puissent être chargés."
|
|
8054
|
+
},
|
|
8055
|
+
"empty": {
|
|
8056
|
+
"title": "Rien à comparer"
|
|
8057
|
+
},
|
|
8058
|
+
"loadFailed": "Impossible de charger les candidats générés."
|
|
8039
8059
|
}
|
|
8040
8060
|
}
|