@cat-factory/app 0.110.3 → 0.110.4

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.
@@ -40,7 +40,7 @@ const { linkPending } = useContextLinking()
40
40
  const open = computed({
41
41
  get: () => ui.addTaskContainerId !== null,
42
42
  set: (v: boolean) => {
43
- if (!v) ui.closeAddTask()
43
+ if (!v) void requestClose()
44
44
  },
45
45
  })
46
46
 
@@ -405,6 +405,34 @@ watch(open, (isOpen) => {
405
405
  resolvePendingIssueBodies().catch(() => {})
406
406
  })
407
407
 
408
+ // UX-18: prompt before discarding typed input on Escape / backdrop / Cancel. Registered
409
+ // after the reset watcher so the baseline is the seeded form (a prefill is the clean
410
+ // starting point, not a spurious edit). The snapshot covers the user-owned fields — the
411
+ // cheap task-type / technical toggles are excluded, and only the *stable* context keys are
412
+ // compared so the async issue-body resolution never reads as a change.
413
+ const { requestClose } = useUnsavedGuard({
414
+ open,
415
+ close: () => ui.closeAddTask(),
416
+ saving: () => saving.value,
417
+ snapshot: () => ({
418
+ title: title.value.trim(),
419
+ description: description.value.trim(),
420
+ severity: severity.value,
421
+ stepsToReproduce: stepsToReproduce.value.trim(),
422
+ timeboxHours: timeboxHours.value ?? null,
423
+ docKind: docKind.value,
424
+ docAudience: docAudience.value.trim(),
425
+ docTargetPath: docTargetPath.value.trim(),
426
+ docOutlineHints: docOutlineHints.value.trim(),
427
+ docKindFieldValues: { ...docKindFieldValues },
428
+ riskPolicyId: riskPolicyId.value,
429
+ modelPresetId: modelPresetId.value,
430
+ pipelineId: pipelineId.value,
431
+ agentConfig: { ...agentConfigValues.value },
432
+ context: pendingContext.value.map(contextKey),
433
+ }),
434
+ })
435
+
408
436
  // A recurring task only needs a target frame (its details are filled in the schedule
409
437
  // modal); every other type needs a title.
410
438
  const canAdd = computed(() =>
@@ -921,7 +949,7 @@ async function add() {
921
949
 
922
950
  <template #footer>
923
951
  <div class="flex w-full justify-end gap-2">
924
- <UButton color="neutral" variant="ghost" @click="ui.closeAddTask()">{{
952
+ <UButton color="neutral" variant="ghost" @click="requestClose()">{{
925
953
  t('common.cancel')
926
954
  }}</UButton>
927
955
  <UButton
@@ -21,7 +21,7 @@ const { t } = useI18n()
21
21
  const open = computed({
22
22
  get: () => ui.addRecurringFrameId !== null,
23
23
  set: (v: boolean) => {
24
- if (!v) ui.closeAddRecurring()
24
+ if (!v) void requestClose()
25
25
  },
26
26
  })
27
27
 
@@ -131,6 +131,33 @@ watch(open, (isOpen) => {
131
131
  void tasks.probe()
132
132
  })
133
133
 
134
+ // UX-18: prompt before discarding typed input on Escape / backdrop / Cancel. Registered
135
+ // after the reset watcher so the baseline is the seeded form (the default pipeline + the
136
+ // workspace tracker settings are the clean starting point, not a spurious edit).
137
+ const { requestClose } = useUnsavedGuard({
138
+ open,
139
+ close: () => ui.closeAddRecurring(),
140
+ saving: () => saving.value,
141
+ snapshot: () => ({
142
+ name: name.value.trim(),
143
+ description: description.value.trim(),
144
+ pipelineId: pipelineId.value,
145
+ onDemand: onDemand.value,
146
+ recurrence: recurrence.value,
147
+ trackerKind: trackerKind.value,
148
+ jiraProjectKey: jiraProjectKey.value.trim(),
149
+ linearTeamId: linearTeamId.value.trim(),
150
+ intakeSource: intakeSource.value,
151
+ intakeJiraProjectKey: intakeJiraProjectKey.value.trim(),
152
+ intakeLinearTeamId: intakeLinearTeamId.value.trim(),
153
+ intakeGithubRepo: intakeGithubRepo.value.trim(),
154
+ intakeTitleFragment: intakeTitleFragment.value.trim(),
155
+ intakeLabels: intakeLabels.value.trim(),
156
+ intakeIssueType: intakeIssueType.value.trim(),
157
+ intakeInProgressLabel: intakeInProgressLabel.value.trim(),
158
+ }),
159
+ })
160
+
134
161
  // The board field required for the picked source must be filled before a bug-intake schedule saves.
135
162
  const intakeReady = computed(() => {
136
163
  if (!isBugIntake.value) return true
@@ -423,7 +450,7 @@ async function add() {
423
450
 
424
451
  <template #footer>
425
452
  <div class="flex w-full justify-end gap-2">
426
- <UButton color="neutral" variant="ghost" @click="ui.closeAddRecurring()">{{
453
+ <UButton color="neutral" variant="ghost" @click="requestClose()">{{
427
454
  t('common.cancel')
428
455
  }}</UButton>
429
456
  <UButton
@@ -22,7 +22,7 @@ const { confirmAction, toastDone } = useConfirmAction()
22
22
  const open = computed({
23
23
  get: () => ui.bootstrapOpen,
24
24
  set: (v: boolean) => {
25
- if (!v) ui.closeBootstrap()
25
+ if (!v) void requestClose()
26
26
  },
27
27
  })
28
28
 
@@ -84,6 +84,23 @@ const typeItems = useFrameRepoTypeItems()
84
84
 
85
85
  const usingReference = computed(() => mode.value === 'reference')
86
86
 
87
+ // UX-18: prompt before discarding a half-filled launch form on Escape / backdrop / the X.
88
+ // The modal keeps its fields across opens (no reset watcher), so the baseline is whatever
89
+ // the form held when it opened — a close only prompts once the user has typed something
90
+ // new. Only the typed launch fields are guarded; the reference-architecture sub-form has
91
+ // its own Cancel/Save. Declared here (below the launch-form refs) because the guard reads
92
+ // its initial baseline synchronously.
93
+ const { requestClose } = useUnsavedGuard({
94
+ open,
95
+ close: () => ui.closeBootstrap(),
96
+ saving: () => launching.value,
97
+ snapshot: () => ({
98
+ repoName: repoName.value.trim(),
99
+ description: description.value.trim(),
100
+ instructions: instructions.value.trim(),
101
+ }),
102
+ })
103
+
87
104
  // Mirror of the backend `slugField` rule (@cat-factory/contracts bootstrap
88
105
  // schema): the new repo name is a SINGLE GitHub name segment — no "owner/"
89
106
  // prefix — so reject a bad value inline before we hit the API. Kept in sync with
@@ -4,6 +4,7 @@ import { agentKindMeta } from '~/utils/catalog'
4
4
  const execution = useExecutionStore()
5
5
  const board = useBoardStore()
6
6
  const ui = useUiStore()
7
+ const toast = useToast()
7
8
  const { t } = useI18n()
8
9
 
9
10
  const ctx = computed(() => ui.decisionContext)
@@ -16,17 +17,42 @@ const decision = computed(() => step.value?.decision ?? null)
16
17
  const block = computed(() => (instance.value ? board.getBlock(instance.value.blockId) : undefined))
17
18
  const agent = computed(() => (step.value ? agentKindMeta(step.value.agentKind) : null))
18
19
 
20
+ // UX-25: which option is being resolved (null = idle). Guards against a fire-and-forget
21
+ // double-submit — the resolve is awaited, all options disable while it runs, and a failed
22
+ // resolve keeps the modal open with an error toast instead of closing silently.
23
+ const resolvingOption = ref<string | null>(null)
24
+
19
25
  const open = computed({
20
26
  get: () => !!ctx.value && !!decision.value,
21
27
  set: (v: boolean) => {
22
- if (!v) ui.closeDecision()
28
+ // While a resolve is in flight the options are disabled, so the dismiss affordances
29
+ // (Escape / backdrop) are locked too — the awaited resolve settles the modal itself.
30
+ if (!v && !resolvingOption.value) ui.closeDecision()
23
31
  },
24
32
  })
25
33
 
26
- function choose(option: string) {
27
- if (!ctx.value) return
28
- execution.resolveDecision(ctx.value.instanceId, ctx.value.decisionId, option)
29
- ui.closeDecision()
34
+ async function choose(option: string) {
35
+ if (!ctx.value || resolvingOption.value) return
36
+ resolvingOption.value = option
37
+ try {
38
+ // `resolveDecision` returns false when a required-credential prompt is cancelled — keep
39
+ // the modal open in that case so the choice isn't silently dropped.
40
+ const resolved = await execution.resolveDecision(
41
+ ctx.value.instanceId,
42
+ ctx.value.decisionId,
43
+ option,
44
+ )
45
+ if (resolved) ui.closeDecision()
46
+ } catch (e) {
47
+ toast.add({
48
+ title: t('panels.decision.resolveFailed'),
49
+ description: e instanceof Error ? e.message : String(e),
50
+ icon: 'i-lucide-triangle-alert',
51
+ color: 'error',
52
+ })
53
+ } finally {
54
+ resolvingOption.value = null
55
+ }
30
56
  }
31
57
  </script>
32
58
 
@@ -65,6 +91,8 @@ function choose(option: string) {
65
91
  block
66
92
  data-testid="decision-option"
67
93
  class="justify-start"
94
+ :loading="resolvingOption === opt"
95
+ :disabled="resolvingOption !== null && resolvingOption !== opt"
68
96
  @click="choose(opt)"
69
97
  >
70
98
  {{ opt }}
@@ -0,0 +1,104 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { nextTick, ref } from 'vue'
3
+ import { useUnsavedGuard } from '~/composables/useUnsavedGuard'
4
+
5
+ // Control the shared confirm dialog: `confirm()` resolves to the user's choice (discard
6
+ // vs keep). `useI18n` is already stubbed to a passthrough in test/setup.ts.
7
+ function stubConfirm(result: boolean) {
8
+ const confirm = vi.fn().mockResolvedValue(result)
9
+ vi.stubGlobal('useConfirm', () => ({ confirm }))
10
+ return confirm
11
+ }
12
+
13
+ describe('useUnsavedGuard', () => {
14
+ it('closes immediately without prompting when the form is unchanged', async () => {
15
+ const confirm = stubConfirm(true)
16
+ const close = vi.fn()
17
+ const value = ref('hello')
18
+ const { requestClose } = useUnsavedGuard({
19
+ open: ref(true),
20
+ close,
21
+ snapshot: () => ({ value: value.value }),
22
+ })
23
+
24
+ await requestClose()
25
+
26
+ expect(confirm).not.toHaveBeenCalled()
27
+ expect(close).toHaveBeenCalledOnce()
28
+ })
29
+
30
+ it('prompts and closes when dirty and the user confirms discard', async () => {
31
+ const confirm = stubConfirm(true)
32
+ const close = vi.fn()
33
+ const value = ref('hello')
34
+ const { requestClose, isDirty } = useUnsavedGuard({
35
+ open: ref(true),
36
+ close,
37
+ snapshot: () => ({ value: value.value }),
38
+ })
39
+
40
+ value.value = 'changed'
41
+ expect(isDirty()).toBe(true)
42
+ await requestClose()
43
+
44
+ expect(confirm).toHaveBeenCalledOnce()
45
+ expect(close).toHaveBeenCalledOnce()
46
+ })
47
+
48
+ it('prompts and keeps the modal open when the user cancels discard', async () => {
49
+ const confirm = stubConfirm(false)
50
+ const close = vi.fn()
51
+ const value = ref('hello')
52
+ const { requestClose } = useUnsavedGuard({
53
+ open: ref(true),
54
+ close,
55
+ snapshot: () => ({ value: value.value }),
56
+ })
57
+
58
+ value.value = 'changed'
59
+ await requestClose()
60
+
61
+ expect(confirm).toHaveBeenCalledOnce()
62
+ expect(close).not.toHaveBeenCalled()
63
+ })
64
+
65
+ it('never prompts or closes while a submit is in flight', async () => {
66
+ const confirm = stubConfirm(true)
67
+ const close = vi.fn()
68
+ const value = ref('hello')
69
+ const { requestClose } = useUnsavedGuard({
70
+ open: ref(true),
71
+ close,
72
+ saving: () => true,
73
+ snapshot: () => ({ value: value.value }),
74
+ })
75
+
76
+ value.value = 'changed'
77
+ await requestClose()
78
+
79
+ expect(confirm).not.toHaveBeenCalled()
80
+ expect(close).not.toHaveBeenCalled()
81
+ })
82
+
83
+ it('re-baselines the seeded form each time the modal opens', async () => {
84
+ stubConfirm(true)
85
+ const open = ref(false)
86
+ const value = ref('')
87
+ const { isDirty } = useUnsavedGuard({
88
+ open,
89
+ close: vi.fn(),
90
+ snapshot: () => ({ value: value.value }),
91
+ })
92
+
93
+ // A reset watcher seeds a prefill; the guard re-snapshots on open, so the seeded value
94
+ // is the clean baseline rather than a spurious edit.
95
+ value.value = 'seeded prefill'
96
+ open.value = true
97
+ await nextTick()
98
+ expect(isDirty()).toBe(false)
99
+
100
+ // A genuine edit after the modal is open is dirty.
101
+ value.value = 'user typed more'
102
+ expect(isDirty()).toBe(true)
103
+ })
104
+ })
@@ -0,0 +1,66 @@
1
+ import { watch } from 'vue'
2
+ import type { Ref } from 'vue'
3
+
4
+ /**
5
+ * Guard a content-heavy modal against silently discarding unsaved input when the user
6
+ * dismisses it (Escape, backdrop click, or a Cancel button). Wire it into a controlled
7
+ * `UModal` whose `open` is a store-backed writable computed: route the setter's close and
8
+ * the Cancel button through `requestClose()` instead of closing directly.
9
+ *
10
+ * `snapshot()` returns a serialisable view of the user-facing form state. The baseline is
11
+ * captured every time the modal opens, so register this AFTER the component's own reset
12
+ * watcher — it then snapshots the *seeded* form (a prefill or an existing edit is the
13
+ * clean baseline, not a spurious change). A close request only prompts when the current
14
+ * snapshot diverges from that baseline; when nothing changed — or a submit is in flight —
15
+ * the close proceeds immediately, so the common path is unchanged.
16
+ *
17
+ * Keep `snapshot()` to stable, user-owned values: exclude fields mutated by async loads
18
+ * (they would read as dirty the instant a background fetch settles) and prefer stable ids
19
+ * over objects that a best-effort resolve rewrites in place.
20
+ */
21
+ export function useUnsavedGuard(opts: {
22
+ /** The modal's open state (the writable computed's underlying getter). */
23
+ open: Ref<boolean>
24
+ /** A serialisable view of the current form state. */
25
+ snapshot: () => unknown
26
+ /** Actually close the modal (the store close action). */
27
+ close: () => void
28
+ /** True while a submit is in flight — a close is then a no-op (the submit closes itself). */
29
+ saving?: () => boolean
30
+ }) {
31
+ const { confirm } = useConfirm()
32
+ const { t } = useI18n()
33
+
34
+ let baseline = serialize(opts.snapshot())
35
+ watch(opts.open, (isOpen) => {
36
+ if (isOpen) baseline = serialize(opts.snapshot())
37
+ })
38
+
39
+ function isDirty(): boolean {
40
+ return serialize(opts.snapshot()) !== baseline
41
+ }
42
+
43
+ async function requestClose(): Promise<void> {
44
+ // A submit in flight closes itself on success — don't interrupt it or prompt.
45
+ if (opts.saving?.()) return
46
+ if (!isDirty()) {
47
+ opts.close()
48
+ return
49
+ }
50
+ const discard = await confirm({
51
+ title: t('common.discard.title'),
52
+ description: t('common.discard.body'),
53
+ confirmLabel: t('common.discard.confirm'),
54
+ cancelLabel: t('common.discard.keep'),
55
+ variant: 'destructive',
56
+ icon: 'i-lucide-triangle-alert',
57
+ })
58
+ if (discard) opts.close()
59
+ }
60
+
61
+ return { requestClose, isDirty }
62
+ }
63
+
64
+ function serialize(value: unknown): string {
65
+ return JSON.stringify(value ?? null)
66
+ }
@@ -1217,7 +1217,8 @@
1217
1217
  "decision": {
1218
1218
  "title": "Entscheidung erforderlich",
1219
1219
  "agentOnBlock": "{agent} auf {block}",
1220
- "visualizationHint": "Dies ist eine Visualisierung. Jede Wahl setzt einfach die Pipeline fort."
1220
+ "visualizationHint": "Dies ist eine Visualisierung. Jede Wahl setzt einfach die Pipeline fort.",
1221
+ "resolveFailed": "Entscheidung konnte nicht gespeichert werden"
1221
1222
  },
1222
1223
  "stepRestart": {
1223
1224
  "restartFromStep": "Pipeline ab diesem Schritt neu starten",
@@ -3656,7 +3657,13 @@
3656
3657
  "undo": "Rückgängig",
3657
3658
  "back": "Zurück",
3658
3659
  "next": "Weiter",
3659
- "done": "Fertig"
3660
+ "done": "Fertig",
3661
+ "discard": {
3662
+ "title": "Änderungen verwerfen?",
3663
+ "body": "Du hast Änderungen vorgenommen, die noch nicht gespeichert wurden. Schließen und verlieren?",
3664
+ "confirm": "Verwerfen",
3665
+ "keep": "Weiter bearbeiten"
3666
+ }
3660
3667
  },
3661
3668
  "clarification": {
3662
3669
  "answerPlaceholder": "Ihre Antwort",
@@ -74,7 +74,13 @@
74
74
  "undo": "Undo",
75
75
  "back": "Back",
76
76
  "next": "Next",
77
- "done": "Done"
77
+ "done": "Done",
78
+ "discard": {
79
+ "title": "Discard your changes?",
80
+ "body": "You've made changes that haven't been saved. Close this and lose them?",
81
+ "confirm": "Discard",
82
+ "keep": "Keep editing"
83
+ }
78
84
  },
79
85
  "clarification": {
80
86
  "answerPlaceholder": "Your answer",
@@ -961,7 +967,8 @@
961
967
  "decision": {
962
968
  "title": "Decision required",
963
969
  "agentOnBlock": "{agent} on {block}",
964
- "visualizationHint": "This is a visualization. Any choice simply resumes the pipeline."
970
+ "visualizationHint": "This is a visualization. Any choice simply resumes the pipeline.",
971
+ "resolveFailed": "Couldn't record your decision"
965
972
  },
966
973
  "stepRestart": {
967
974
  "restartFromStep": "Restart pipeline from this step",
@@ -65,7 +65,13 @@
65
65
  "undo": "Deshacer",
66
66
  "back": "Atrás",
67
67
  "next": "Siguiente",
68
- "done": "Listo"
68
+ "done": "Listo",
69
+ "discard": {
70
+ "title": "¿Descartar los cambios?",
71
+ "body": "Has hecho cambios que no se han guardado. ¿Cerrar y perderlos?",
72
+ "confirm": "Descartar",
73
+ "keep": "Seguir editando"
74
+ }
69
75
  },
70
76
  "clarification": {
71
77
  "answerPlaceholder": "Tu respuesta",
@@ -907,7 +913,8 @@
907
913
  "decision": {
908
914
  "title": "Se requiere una decisión",
909
915
  "agentOnBlock": "{agent} en {block}",
910
- "visualizationHint": "Esto es una visualización. Cualquier elección simplemente reanuda el pipeline."
916
+ "visualizationHint": "Esto es una visualización. Cualquier elección simplemente reanuda el pipeline.",
917
+ "resolveFailed": "No se pudo registrar tu decisión"
911
918
  },
912
919
  "stepRestart": {
913
920
  "restartFromStep": "Reiniciar el pipeline desde este paso",
@@ -65,7 +65,13 @@
65
65
  "undo": "Annuler",
66
66
  "back": "Retour",
67
67
  "next": "Suivant",
68
- "done": "Terminé"
68
+ "done": "Terminé",
69
+ "discard": {
70
+ "title": "Ignorer vos modifications ?",
71
+ "body": "Vous avez effectué des modifications non enregistrées. Fermer et les perdre ?",
72
+ "confirm": "Ignorer",
73
+ "keep": "Continuer l'édition"
74
+ }
69
75
  },
70
76
  "clarification": {
71
77
  "answerPlaceholder": "Votre réponse",
@@ -907,7 +913,8 @@
907
913
  "decision": {
908
914
  "title": "Décision requise",
909
915
  "agentOnBlock": "{agent} sur {block}",
910
- "visualizationHint": "Ceci est une visualisation. Tout choix reprend simplement le pipeline."
916
+ "visualizationHint": "Ceci est une visualisation. Tout choix reprend simplement le pipeline.",
917
+ "resolveFailed": "Impossible d'enregistrer votre décision"
911
918
  },
912
919
  "stepRestart": {
913
920
  "restartFromStep": "Redémarrer le pipeline à partir de cette étape",
@@ -65,7 +65,13 @@
65
65
  "undo": "בטל",
66
66
  "back": "חזרה",
67
67
  "next": "הבא",
68
- "done": "סיום"
68
+ "done": "סיום",
69
+ "discard": {
70
+ "title": "לבטל את השינויים?",
71
+ "body": "ביצעת שינויים שלא נשמרו. לסגור ולאבד אותם?",
72
+ "confirm": "לבטל",
73
+ "keep": "להמשיך לערוך"
74
+ }
69
75
  },
70
76
  "clarification": {
71
77
  "answerPlaceholder": "התשובה שלך",
@@ -907,7 +913,8 @@
907
913
  "decision": {
908
914
  "title": "נדרשת החלטה",
909
915
  "agentOnBlock": "{agent} על {block}",
910
- "visualizationHint": "זוהי הצגה חזותית. כל בחירה פשוט ממשיכה את הצינור."
916
+ "visualizationHint": "זוהי הצגה חזותית. כל בחירה פשוט ממשיכה את הצינור.",
917
+ "resolveFailed": "לא ניתן היה לשמור את ההחלטה שלך"
911
918
  },
912
919
  "stepRestart": {
913
920
  "restartFromStep": "הפעל מחדש את הצינור משלב זה",
@@ -1217,7 +1217,8 @@
1217
1217
  "decision": {
1218
1218
  "title": "Decisione richiesta",
1219
1219
  "agentOnBlock": "{agent} su {block}",
1220
- "visualizationHint": "Questa e' una visualizzazione. Qualsiasi scelta riprende semplicemente la pipeline."
1220
+ "visualizationHint": "Questa e' una visualizzazione. Qualsiasi scelta riprende semplicemente la pipeline.",
1221
+ "resolveFailed": "Impossibile registrare la tua decisione"
1221
1222
  },
1222
1223
  "stepRestart": {
1223
1224
  "restartFromStep": "Riavvia la pipeline da questo passaggio",
@@ -3656,7 +3657,13 @@
3656
3657
  "undo": "Annulla",
3657
3658
  "back": "Indietro",
3658
3659
  "next": "Avanti",
3659
- "done": "Fatto"
3660
+ "done": "Fatto",
3661
+ "discard": {
3662
+ "title": "Ignorare le modifiche?",
3663
+ "body": "Hai apportato modifiche non salvate. Chiudere e perderle?",
3664
+ "confirm": "Ignora",
3665
+ "keep": "Continua a modificare"
3666
+ }
3660
3667
  },
3661
3668
  "clarification": {
3662
3669
  "answerPlaceholder": "La tua risposta",
@@ -65,7 +65,13 @@
65
65
  "undo": "元に戻す",
66
66
  "back": "戻る",
67
67
  "next": "次へ",
68
- "done": "完了"
68
+ "done": "完了",
69
+ "discard": {
70
+ "title": "変更を破棄しますか?",
71
+ "body": "保存されていない変更があります。閉じて破棄しますか?",
72
+ "confirm": "破棄",
73
+ "keep": "編集を続ける"
74
+ }
69
75
  },
70
76
  "clarification": {
71
77
  "answerPlaceholder": "回答を入力",
@@ -907,7 +913,8 @@
907
913
  "decision": {
908
914
  "title": "判断が必要",
909
915
  "agentOnBlock": "{block} の {agent}",
910
- "visualizationHint": "これは可視化です。どの選択でもパイプラインを再開するだけです。"
916
+ "visualizationHint": "これは可視化です。どの選択でもパイプラインを再開するだけです。",
917
+ "resolveFailed": "決定を記録できませんでした"
911
918
  },
912
919
  "stepRestart": {
913
920
  "restartFromStep": "このステップからパイプラインを再開",
@@ -65,7 +65,13 @@
65
65
  "undo": "Cofnij",
66
66
  "back": "Wstecz",
67
67
  "next": "Dalej",
68
- "done": "Gotowe"
68
+ "done": "Gotowe",
69
+ "discard": {
70
+ "title": "Odrzucić zmiany?",
71
+ "body": "Masz niezapisane zmiany. Zamknąć i je utracić?",
72
+ "confirm": "Odrzuć",
73
+ "keep": "Kontynuuj edycję"
74
+ }
69
75
  },
70
76
  "clarification": {
71
77
  "answerPlaceholder": "Twoja odpowiedź",
@@ -907,7 +913,8 @@
907
913
  "decision": {
908
914
  "title": "Wymagana decyzja",
909
915
  "agentOnBlock": "{agent} na {block}",
910
- "visualizationHint": "To jest wizualizacja. Każdy wybór po prostu wznawia potok."
916
+ "visualizationHint": "To jest wizualizacja. Każdy wybór po prostu wznawia potok.",
917
+ "resolveFailed": "Nie udało się zapisać decyzji"
911
918
  },
912
919
  "stepRestart": {
913
920
  "restartFromStep": "Uruchom ponownie potok od tego kroku",
@@ -65,7 +65,13 @@
65
65
  "undo": "Geri al",
66
66
  "back": "Geri",
67
67
  "next": "İleri",
68
- "done": "Bitti"
68
+ "done": "Bitti",
69
+ "discard": {
70
+ "title": "Değişiklikler atılsın mı?",
71
+ "body": "Kaydedilmemiş değişiklikleriniz var. Kapatıp bunları kaybedecek misiniz?",
72
+ "confirm": "At",
73
+ "keep": "Düzenlemeye devam et"
74
+ }
69
75
  },
70
76
  "clarification": {
71
77
  "answerPlaceholder": "Yanıtınız",
@@ -907,7 +913,8 @@
907
913
  "decision": {
908
914
  "title": "Karar gerekiyor",
909
915
  "agentOnBlock": "{block} üzerinde {agent}",
910
- "visualizationHint": "Bu bir görselleştirmedir. Herhangi bir seçim pipeline'ı yeniden başlatır."
916
+ "visualizationHint": "Bu bir görselleştirmedir. Herhangi bir seçim pipeline'ı yeniden başlatır.",
917
+ "resolveFailed": "Kararınız kaydedilemedi"
911
918
  },
912
919
  "stepRestart": {
913
920
  "restartFromStep": "Pipeline'ı bu adımdan yeniden başlat",
@@ -65,7 +65,13 @@
65
65
  "undo": "Відмінити",
66
66
  "back": "Назад",
67
67
  "next": "Далі",
68
- "done": "Готово"
68
+ "done": "Готово",
69
+ "discard": {
70
+ "title": "Відхилити зміни?",
71
+ "body": "Ви внесли зміни, які не збережено. Закрити та втратити їх?",
72
+ "confirm": "Відхилити",
73
+ "keep": "Продовжити редагування"
74
+ }
69
75
  },
70
76
  "clarification": {
71
77
  "answerPlaceholder": "Ваша відповідь",
@@ -907,7 +913,8 @@
907
913
  "decision": {
908
914
  "title": "Потрібне рішення",
909
915
  "agentOnBlock": "{agent} на {block}",
910
- "visualizationHint": "Це візуалізація. Будь-який вибір просто відновлює конвеєр."
916
+ "visualizationHint": "Це візуалізація. Будь-який вибір просто відновлює конвеєр.",
917
+ "resolveFailed": "Не вдалося зафіксувати ваше рішення"
911
918
  },
912
919
  "stepRestart": {
913
920
  "restartFromStep": "Перезапустити конвеєр із цього кроку",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.110.3",
3
+ "version": "0.110.4",
4
4
  "description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
5
5
  "repository": {
6
6
  "type": "git",