@cat-factory/app 0.13.0 → 0.14.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.
@@ -12,8 +12,16 @@ const github = useGitHubStore()
12
12
  const slack = useSlackStore()
13
13
  const documents = useDocumentsStore()
14
14
  const tasks = useTasksStore()
15
+ const tracker = useTrackerStore()
15
16
  const releaseHealth = useReleaseHealthStore()
16
17
 
18
+ // The selected filing tracker, as a badge label ("GitHub Issues" / "Jira").
19
+ const trackerLabel = computed(() => {
20
+ if (tracker.settings.tracker === 'github') return 'GitHub Issues'
21
+ if (tracker.settings.tracker === 'jira') return 'Jira'
22
+ return undefined
23
+ })
24
+
17
25
  // The observability connection status drives the hub's connected badge. Load it
18
26
  // lazily when the hub opens (the secret-less connection view is cheap).
19
27
  watch(
@@ -130,11 +138,13 @@ const groups = computed<IntegrationGroup[]>(() => {
130
138
  })
131
139
  }
132
140
  trackers.push({
133
- key: 'task:writeback',
134
- icon: 'i-lucide-message-square-reply',
135
- label: 'Issue tracker writeback',
136
- description: 'Comment on the PR and close the linked issue on merge.',
137
- onClick: () => go(() => ui.openWorkspaceSettings('writeback')),
141
+ key: 'task:tracker',
142
+ icon: 'i-lucide-list-checks',
143
+ label: 'Issue tracker settings',
144
+ description: 'Choose the filing tracker, enable linking sources, and configure writeback.',
145
+ status: trackerLabel.value,
146
+ connected: tracker.settings.tracker !== null,
147
+ onClick: () => go(() => ui.openWorkspaceSettings('tracker')),
138
148
  })
139
149
  out.push({ title: 'Task trackers', items: trackers })
140
150
  }
@@ -0,0 +1,393 @@
1
+ <script setup lang="ts">
2
+ // Workspace settings: a single, first-class home for issue tracking. It gathers
3
+ // the three things that used to be scattered (and, for the filing tracker, were
4
+ // only reachable buried inside the tech-debt recurring-pipeline modal):
5
+ //
6
+ // 1. Filing tracker — which tracker the tech-debt pipeline's `tracker` step
7
+ // files its ticket in (GitHub Issues / Jira / none).
8
+ // 2. Linking sources — the per-workspace on/off toggle for each source
9
+ // (task_source_settings) that governs whether issues can be imported and
10
+ // linked to tasks as agent context.
11
+ // 3. Writeback — comment on a task's linked issue when its PR opens, and
12
+ // comment + close it when the PR merges.
13
+ //
14
+ // Filing and linking are independent (filing rides the App / Jira connection
15
+ // directly; linking is the source toggle), so both are shown explicitly to undo
16
+ // the common confusion that "I have the GitHub App, why is nothing surfaced?".
17
+ import { computed, onMounted, ref, watch } from 'vue'
18
+ import type { TaskSourceDiagnosticStatus, TaskSourceKind, TrackerKind } from '~/types/domain'
19
+
20
+ const tracker = useTrackerStore()
21
+ const tasks = useTasksStore()
22
+ const ui = useUiStore()
23
+ const toast = useToast()
24
+
25
+ // --- filing tracker + writeback (one Save, persisted on tracker settings) -----
26
+ const trackerKind = ref<TrackerKind | null>(null)
27
+ const jiraProjectKey = ref('')
28
+ const commentOnPrOpen = ref(false)
29
+ const resolveOnMerge = ref(false)
30
+ const saving = ref(false)
31
+
32
+ function hydrate() {
33
+ trackerKind.value = tracker.settings.tracker
34
+ jiraProjectKey.value = tracker.settings.jiraProjectKey ?? ''
35
+ commentOnPrOpen.value = tracker.settings.writebackCommentOnPrOpen
36
+ resolveOnMerge.value = tracker.settings.writebackResolveOnMerge
37
+ }
38
+ onMounted(() => {
39
+ hydrate()
40
+ // The descriptors (availability + enable state) come from the task-source probe;
41
+ // probe on open if the navbar hasn't already, so the toggles below reflect reality.
42
+ if (tasks.available === null) void tasks.probe()
43
+ })
44
+ watch(() => tracker.settings, hydrate, { deep: true })
45
+
46
+ // Per-source live state (available = usable now; enabled = offered to the workspace).
47
+ const github = computed(() => tasks.descriptorFor('github'))
48
+ const jira = computed(() => tasks.descriptorFor('jira'))
49
+
50
+ // A tracker can only file where it can authenticate: GitHub rides the installed
51
+ // App, Jira needs a connection. Selecting an unusable tracker is allowed (it just
52
+ // won't file until set up), but we surface the gap inline.
53
+ const githubAvailable = computed(() => github.value?.available ?? false)
54
+ const jiraConnected = computed(() => tasks.isConnected('jira'))
55
+
56
+ // Jira needs a project key to file into; block Save on an empty one when picked.
57
+ const canSave = computed(
58
+ () => trackerKind.value !== 'jira' || jiraProjectKey.value.trim().length > 0,
59
+ )
60
+
61
+ async function save() {
62
+ if (!canSave.value) return
63
+ saving.value = true
64
+ try {
65
+ await tracker.save({
66
+ tracker: trackerKind.value,
67
+ jiraProjectKey: trackerKind.value === 'jira' ? jiraProjectKey.value.trim() : null,
68
+ writebackCommentOnPrOpen: commentOnPrOpen.value,
69
+ writebackResolveOnMerge: resolveOnMerge.value,
70
+ })
71
+ toast.add({ title: 'Issue tracker saved', icon: 'i-lucide-check', color: 'success' })
72
+ } catch (e) {
73
+ toast.add({
74
+ title: 'Could not save settings',
75
+ description: e instanceof Error ? e.message : String(e),
76
+ icon: 'i-lucide-triangle-alert',
77
+ color: 'error',
78
+ })
79
+ } finally {
80
+ saving.value = false
81
+ }
82
+ }
83
+
84
+ // --- linking sources (per-source toggle, saved immediately) -------------------
85
+ const togglingSource = ref<TaskSourceKind | null>(null)
86
+
87
+ async function toggleSource(source: TaskSourceKind, enabled: boolean) {
88
+ togglingSource.value = source
89
+ try {
90
+ await tasks.setEnabled(source, enabled)
91
+ } catch (e) {
92
+ toast.add({
93
+ title: 'Could not update',
94
+ description: e instanceof Error ? e.message : String(e),
95
+ icon: 'i-lucide-triangle-alert',
96
+ color: 'error',
97
+ })
98
+ } finally {
99
+ togglingSource.value = null
100
+ }
101
+ }
102
+
103
+ // --- live "check setup" -------------------------------------------------------
104
+ // The probe failed entirely (not a per-source state): the whole integration is
105
+ // off or the backend errored, so the panel can't show real source state. We
106
+ // translate the captured status into a plain explanation + next step.
107
+ const probeFailureHint = computed(() => {
108
+ const err = tasks.probeError
109
+ if (tasks.available !== false || !err) return null
110
+ if (err.status === 503) {
111
+ return 'The task-source integration is turned off on this deployment (its encryption key is not configured). Set ENCRYPTION_KEY on the backend to enable issue tracking.'
112
+ }
113
+ if (err.status && err.status >= 500) {
114
+ return `The issue-tracker service returned an error (HTTP ${err.status}): ${err.message}. This usually means the backend isn't fully migrated/configured.`
115
+ }
116
+ return `Couldn't load issue-tracker settings${err.status ? ` (HTTP ${err.status})` : ''}: ${err.message}`
117
+ })
118
+
119
+ async function checkSetup(source: TaskSourceKind) {
120
+ try {
121
+ await tasks.checkSetup(source)
122
+ } catch (e) {
123
+ toast.add({
124
+ title: 'Check failed',
125
+ description: e instanceof Error ? e.message : String(e),
126
+ icon: 'i-lucide-triangle-alert',
127
+ color: 'error',
128
+ })
129
+ }
130
+ }
131
+
132
+ // Status → presentation for a setup-check verdict.
133
+ const STATUS_UI: Record<
134
+ TaskSourceDiagnosticStatus,
135
+ { color: 'success' | 'warning' | 'error' | 'neutral'; icon: string }
136
+ > = {
137
+ ready: { color: 'success', icon: 'i-lucide-circle-check' },
138
+ not_installed: { color: 'warning', icon: 'i-lucide-download' },
139
+ not_connected: { color: 'warning', icon: 'i-lucide-plug' },
140
+ auth_failed: { color: 'error', icon: 'i-lucide-key-round' },
141
+ forbidden: { color: 'error', icon: 'i-lucide-shield-x' },
142
+ unreachable: { color: 'error', icon: 'i-lucide-wifi-off' },
143
+ error: { color: 'error', icon: 'i-lucide-triangle-alert' },
144
+ }
145
+ </script>
146
+
147
+ <template>
148
+ <div class="space-y-7">
149
+ <!-- Whole-integration failure: explain WHY nothing is surfaced, instead of the
150
+ passive per-source "install first" hints (which would be misleading here). -->
151
+ <UAlert
152
+ v-if="probeFailureHint"
153
+ color="error"
154
+ variant="subtle"
155
+ icon="i-lucide-triangle-alert"
156
+ title="Issue tracking isn't available"
157
+ :description="probeFailureHint"
158
+ />
159
+
160
+ <!-- 1. Filing tracker ----------------------------------------------------->
161
+ <section class="space-y-3">
162
+ <div>
163
+ <h3 class="text-sm font-semibold text-slate-200">Where tickets are filed</h3>
164
+ <p class="mt-1 text-[11px] text-slate-400">
165
+ The tech-debt recurring pipeline raises an issue before implementation and files it in
166
+ this tracker. Choose <span class="text-slate-300">None</span> to skip filing.
167
+ </p>
168
+ </div>
169
+
170
+ <div class="flex flex-wrap gap-2">
171
+ <UButton
172
+ icon="i-lucide-circle-slash"
173
+ size="sm"
174
+ :color="trackerKind === null ? 'primary' : 'neutral'"
175
+ :variant="trackerKind === null ? 'solid' : 'subtle'"
176
+ @click="trackerKind = null"
177
+ >
178
+ None
179
+ </UButton>
180
+ <UButton
181
+ icon="i-lucide-github"
182
+ size="sm"
183
+ :color="trackerKind === 'github' ? 'primary' : 'neutral'"
184
+ :variant="trackerKind === 'github' ? 'solid' : 'subtle'"
185
+ @click="trackerKind = 'github'"
186
+ >
187
+ GitHub Issues
188
+ </UButton>
189
+ <UButton
190
+ icon="i-lucide-trello"
191
+ size="sm"
192
+ :color="trackerKind === 'jira' ? 'primary' : 'neutral'"
193
+ :variant="trackerKind === 'jira' ? 'solid' : 'subtle'"
194
+ @click="trackerKind = 'jira'"
195
+ >
196
+ Jira
197
+ </UButton>
198
+ </div>
199
+
200
+ <!-- Inline readiness hints for the picked tracker. -->
201
+ <p v-if="trackerKind === 'github' && !githubAvailable" class="text-[11px] text-amber-400">
202
+ GitHub Issues rides your installed GitHub App, which isn't connected yet. Install it under
203
+ <button class="underline" @click="ui.openGitHub()">Integrations → GitHub</button> — filing
204
+ stays off until then.
205
+ </p>
206
+ <p v-else-if="trackerKind === 'jira' && !jiraConnected" class="text-[11px] text-amber-400">
207
+ Jira isn't connected yet.
208
+ <button class="underline" @click="ui.openTaskConnect('jira')">Connect it</button> to file
209
+ and link issues.
210
+ </p>
211
+
212
+ <UFormField v-if="trackerKind === 'jira'" label="Jira project key" class="w-48">
213
+ <UInput v-model="jiraProjectKey" placeholder="ENG" size="sm" class="w-full" />
214
+ <template #help>
215
+ <span class="text-[11px] text-slate-500">New tickets are filed under this project.</span>
216
+ </template>
217
+ </UFormField>
218
+ </section>
219
+
220
+ <!-- 2. Linking sources ---------------------------------------------------->
221
+ <section class="space-y-3">
222
+ <div>
223
+ <h3 class="text-sm font-semibold text-slate-200">Link issues as context</h3>
224
+ <p class="mt-1 text-[11px] text-slate-400">
225
+ When a source is offered you can import its issues and attach them to a task, so agents
226
+ see the issue description and comments while implementing. This is independent of the
227
+ filing tracker above.
228
+ </p>
229
+ </div>
230
+
231
+ <!-- GitHub Issues source -->
232
+ <div class="rounded-lg border border-slate-800 bg-slate-800/40 px-3 py-2.5">
233
+ <div class="flex items-center justify-between gap-2">
234
+ <div class="flex min-w-0 items-center gap-2.5">
235
+ <UIcon name="i-lucide-github" class="h-5 w-5 shrink-0 text-slate-300" />
236
+ <div class="min-w-0">
237
+ <div class="text-sm font-medium text-slate-200">GitHub Issues</div>
238
+ <div class="text-[11px] text-slate-500">
239
+ {{
240
+ githubAvailable
241
+ ? 'Rides your installed GitHub App — no credentials needed.'
242
+ : 'Install the GitHub App (Integrations → GitHub) to offer this source.'
243
+ }}
244
+ </div>
245
+ </div>
246
+ </div>
247
+ <div class="flex shrink-0 items-center gap-2">
248
+ <UButton
249
+ size="xs"
250
+ color="neutral"
251
+ variant="ghost"
252
+ icon="i-lucide-stethoscope"
253
+ :loading="tasks.checking === 'github'"
254
+ @click="checkSetup('github')"
255
+ >
256
+ Check setup
257
+ </UButton>
258
+ <USwitch
259
+ v-if="githubAvailable"
260
+ :model-value="github?.enabled ?? false"
261
+ :loading="togglingSource === 'github'"
262
+ @update:model-value="(v: boolean) => toggleSource('github', v)"
263
+ />
264
+ <UButton
265
+ v-else
266
+ size="xs"
267
+ color="neutral"
268
+ variant="soft"
269
+ icon="i-lucide-github"
270
+ @click="ui.openGitHub()"
271
+ >
272
+ Install
273
+ </UButton>
274
+ </div>
275
+ </div>
276
+ <UAlert
277
+ v-if="tasks.diagnostics.github"
278
+ class="mt-2.5"
279
+ :color="STATUS_UI[tasks.diagnostics.github.status].color"
280
+ variant="subtle"
281
+ :icon="STATUS_UI[tasks.diagnostics.github.status].icon"
282
+ :description="
283
+ tasks.diagnostics.github.message +
284
+ (tasks.diagnostics.github.detail ? ` ${tasks.diagnostics.github.detail}` : '')
285
+ "
286
+ :ui="{ description: 'text-[11px]' }"
287
+ />
288
+ </div>
289
+
290
+ <!-- Jira source -->
291
+ <div class="rounded-lg border border-slate-800 bg-slate-800/40 px-3 py-2.5">
292
+ <div class="flex items-center justify-between gap-2">
293
+ <div class="flex min-w-0 items-center gap-2.5">
294
+ <UIcon name="i-lucide-trello" class="h-5 w-5 shrink-0 text-slate-300" />
295
+ <div class="min-w-0">
296
+ <div class="text-sm font-medium text-slate-200">Jira</div>
297
+ <div class="text-[11px] text-slate-500">
298
+ {{ jiraConnected ? 'Connected.' : 'Connect with a Jira account and API token.' }}
299
+ </div>
300
+ </div>
301
+ </div>
302
+ <div class="flex shrink-0 items-center gap-2">
303
+ <UButton
304
+ v-if="jiraConnected"
305
+ size="xs"
306
+ color="neutral"
307
+ variant="ghost"
308
+ icon="i-lucide-stethoscope"
309
+ :loading="tasks.checking === 'jira'"
310
+ @click="checkSetup('jira')"
311
+ >
312
+ Check setup
313
+ </UButton>
314
+ <USwitch
315
+ v-if="jira?.available"
316
+ :model-value="jira?.enabled ?? false"
317
+ :loading="togglingSource === 'jira'"
318
+ @update:model-value="(v: boolean) => toggleSource('jira', v)"
319
+ />
320
+ <UButton
321
+ v-else
322
+ size="xs"
323
+ color="neutral"
324
+ variant="soft"
325
+ icon="i-lucide-plug"
326
+ @click="ui.openTaskConnect('jira')"
327
+ >
328
+ Connect
329
+ </UButton>
330
+ </div>
331
+ </div>
332
+ <UAlert
333
+ v-if="tasks.diagnostics.jira"
334
+ class="mt-2.5"
335
+ :color="STATUS_UI[tasks.diagnostics.jira.status].color"
336
+ variant="subtle"
337
+ :icon="STATUS_UI[tasks.diagnostics.jira.status].icon"
338
+ :description="
339
+ tasks.diagnostics.jira.message +
340
+ (tasks.diagnostics.jira.detail ? ` ${tasks.diagnostics.jira.detail}` : '')
341
+ "
342
+ :ui="{ description: 'text-[11px]' }"
343
+ />
344
+ </div>
345
+ </section>
346
+
347
+ <!-- 3. Writeback ---------------------------------------------------------->
348
+ <section class="space-y-3">
349
+ <div>
350
+ <h3 class="text-sm font-semibold text-slate-200">Writeback</h3>
351
+ <p class="mt-1 text-[11px] text-slate-400">
352
+ Write back to a task's linked issue(s) as its pull request progresses. Each toggle is the
353
+ workspace default and can be overridden per task in the inspector. GitHub issues close
354
+ natively; Jira issues transition to the first status in their
355
+ <span class="text-slate-300">Done</span> category.
356
+ </p>
357
+ </div>
358
+
359
+ <label class="flex items-start gap-3 rounded-lg border border-slate-700 bg-slate-800/40 p-3">
360
+ <USwitch v-model="commentOnPrOpen" />
361
+ <span class="text-sm">
362
+ <span class="block text-slate-200">Comment when a PR opens</span>
363
+ <span class="block text-xs text-slate-500">
364
+ Post a comment on the linked issue with the new pull request's link.
365
+ </span>
366
+ </span>
367
+ </label>
368
+
369
+ <label class="flex items-start gap-3 rounded-lg border border-slate-700 bg-slate-800/40 p-3">
370
+ <USwitch v-model="resolveOnMerge" />
371
+ <span class="text-sm">
372
+ <span class="block text-slate-200">Close as resolved when a PR merges</span>
373
+ <span class="block text-xs text-slate-500">
374
+ Comment that the PR merged, then close / resolve the linked issue.
375
+ </span>
376
+ </span>
377
+ </label>
378
+ </section>
379
+
380
+ <div class="flex justify-end">
381
+ <UButton
382
+ color="primary"
383
+ icon="i-lucide-save"
384
+ size="sm"
385
+ :loading="saving"
386
+ :disabled="!canSave"
387
+ @click="save"
388
+ >
389
+ Save
390
+ </UButton>
391
+ </div>
392
+ </div>
393
+ </template>
@@ -3,14 +3,14 @@
3
3
  // configuration that used to live in separate windows:
4
4
  // - Workspace: the run-timing escalation threshold + per-service running-task limit.
5
5
  // - Merge thresholds: the auto-merge preset library.
6
- // - Issue writeback: the tracker writeback toggles.
6
+ // - Issue tracker: filing-tracker selection + linking sources + writeback.
7
7
  // - Service best practices: the default fragments new services inherit.
8
8
  // The latter three are body-only section components rendered in tabs here (no longer
9
9
  // standalone modals).
10
10
  import { reactive, ref, watch } from 'vue'
11
11
  import type { CreateTaskType, TaskLimitMode } from '~/types/domain'
12
12
  import MergeThresholdsPanel from '~/components/settings/MergeThresholdsPanel.vue'
13
- import IssueTrackerWritebackPanel from '~/components/settings/IssueTrackerWritebackPanel.vue'
13
+ import IssueTrackerPanel from '~/components/settings/IssueTrackerPanel.vue'
14
14
  import ServiceFragmentDefaultsPanel from '~/components/settings/ServiceFragmentDefaultsPanel.vue'
15
15
 
16
16
  const ui = useUiStore()
@@ -38,10 +38,10 @@ const tabs = [
38
38
  },
39
39
  { value: 'merge', label: 'Merge thresholds', icon: 'i-lucide-git-merge', slot: 'merge' },
40
40
  {
41
- value: 'writeback',
42
- label: 'Issue writeback',
43
- icon: 'i-lucide-message-square-reply',
44
- slot: 'writeback',
41
+ value: 'tracker',
42
+ label: 'Issue tracker',
43
+ icon: 'i-lucide-list-checks',
44
+ slot: 'tracker',
45
45
  },
46
46
  {
47
47
  value: 'fragments',
@@ -200,9 +200,9 @@ async function save() {
200
200
  <MergeThresholdsPanel />
201
201
  </template>
202
202
 
203
- <!-- Issue writeback -->
204
- <template #writeback>
205
- <IssueTrackerWritebackPanel />
203
+ <!-- Issue tracker -->
204
+ <template #tracker>
205
+ <IssueTrackerPanel />
206
206
  </template>
207
207
 
208
208
  <!-- Service best practices -->
@@ -3,6 +3,7 @@ import type {
3
3
  SourceTask,
4
4
  TaskConnection,
5
5
  TaskSearchResult,
6
+ TaskSourceDiagnostic,
6
7
  TaskSourceKind,
7
8
  TaskSourceState,
8
9
  } from '~/types/domain'
@@ -41,6 +42,13 @@ export function tasksApi({ http, ws }: ApiContext) {
41
42
  disconnectTaskSource: (workspaceId: string, source: TaskSourceKind) =>
42
43
  http(`${ws(workspaceId)}/task-sources/${source}/connection`, { method: 'DELETE' }),
43
44
 
45
+ // Live "check setup" probe: authenticates against the source and reads a slice
46
+ // of its issues API, returning a classified verdict the panel renders verbatim.
47
+ checkTaskSource: (workspaceId: string, source: TaskSourceKind) =>
48
+ http<TaskSourceDiagnostic>(`${ws(workspaceId)}/task-sources/${source}/diagnostics`, {
49
+ method: 'POST',
50
+ }),
51
+
44
52
  listTasks: (workspaceId: string) => http<SourceTask[]>(`${ws(workspaceId)}/tasks`),
45
53
 
46
54
  importTask: (workspaceId: string, source: TaskSourceKind, body: { ref: string }) =>
@@ -4,6 +4,7 @@ import type {
4
4
  SourceTask,
5
5
  TaskConnection,
6
6
  TaskSearchResult,
7
+ TaskSourceDiagnostic,
7
8
  TaskSourceKind,
8
9
  TaskSourceState,
9
10
  } from '~/types/domain'
@@ -27,11 +28,21 @@ export const useTasksStore = defineStore('tasks', () => {
27
28
 
28
29
  /** null = unknown (not probed yet), true/false = integration on/off. */
29
30
  const available = ref<boolean | null>(null)
31
+ /**
32
+ * Why the last probe failed, when it did — captured (rather than swallowed) so
33
+ * the settings panel can explain *why* nothing is surfaced (integration disabled
34
+ * vs a server/backend error) instead of a blanket "install integration first".
35
+ */
36
+ const probeError = ref<{ status: number | null; message: string } | null>(null)
30
37
  /** The configured sources, each with its descriptor + per-workspace state (available + enabled). */
31
38
  const sources = ref<TaskSourceState[]>([])
32
39
  /** Live connections, one per connected (credentialed) source. */
33
40
  const connections = ref<TaskConnection[]>([])
34
41
  const tasks = ref<SourceTask[]>([])
42
+ /** The last live setup-check verdict per source (from `checkSetup`). */
43
+ const diagnostics = ref<Partial<Record<TaskSourceKind, TaskSourceDiagnostic>>>({})
44
+ /** The source currently running a setup check, if any. */
45
+ const checking = ref<TaskSourceKind | null>(null)
35
46
  const loading = ref(false)
36
47
 
37
48
  /** Sources the workspace currently has a live connection to. */
@@ -85,16 +96,43 @@ export const useTasksStore = defineStore('tasks', () => {
85
96
  api.listTaskConnections(workspace.requireId()),
86
97
  ])
87
98
  available.value = true
99
+ probeError.value = null
88
100
  sources.value = srcs
89
101
  connections.value = conns
90
- } catch {
91
- // 503 (integration disabled) or any error → hide the UI entry points.
102
+ } catch (e) {
103
+ // 503 (integration disabled) or any error → hide the UI entry points, but keep
104
+ // the reason so the settings panel can explain it (a 503 is "turned off on this
105
+ // deployment"; a 500 is "the backend errored — e.g. a migration isn't applied").
92
106
  available.value = false
107
+ const err = e as { statusCode?: number; data?: { error?: { message?: string } } }
108
+ const serverMessage = err?.data?.error?.message
109
+ probeError.value = {
110
+ status: err?.statusCode ?? null,
111
+ message: serverMessage || (e instanceof Error ? e.message : String(e)),
112
+ }
93
113
  sources.value = []
94
114
  connections.value = []
95
115
  }
96
116
  }
97
117
 
118
+ /**
119
+ * Run a live setup check for a source (authenticate + read), caching the verdict
120
+ * so the panel can show exactly what's wrong (missing App / wrong token / lacking
121
+ * the Issues permission) and how to fix it. Re-probes on success so a
122
+ * just-fixed source flips `available`/`enabled` without a manual reload.
123
+ */
124
+ async function checkSetup(source: TaskSourceKind): Promise<TaskSourceDiagnostic> {
125
+ checking.value = source
126
+ try {
127
+ const result = await api.checkTaskSource(workspace.requireId(), source)
128
+ diagnostics.value = { ...diagnostics.value, [source]: result }
129
+ if (result.ok) await probe()
130
+ return result
131
+ } finally {
132
+ checking.value = null
133
+ }
134
+ }
135
+
98
136
  /** Connect the workspace to a source with its credential bag. */
99
137
  async function connect(source: TaskSourceKind, credentials: Record<string, string>) {
100
138
  const conn = await api.connectTaskSource(workspace.requireId(), source, credentials)
@@ -165,9 +203,12 @@ export const useTasksStore = defineStore('tasks', () => {
165
203
 
166
204
  return {
167
205
  available,
206
+ probeError,
168
207
  sources,
169
208
  connections,
170
209
  tasks,
210
+ diagnostics,
211
+ checking,
171
212
  loading,
172
213
  connectedSources,
173
214
  anyConnected,
@@ -178,6 +219,7 @@ export const useTasksStore = defineStore('tasks', () => {
178
219
  isConnected,
179
220
  tasksForBlock,
180
221
  probe,
222
+ checkSetup,
181
223
  connect,
182
224
  disconnect,
183
225
  setEnabled,
@@ -37,6 +37,31 @@ export interface TaskSourceState extends TaskSourceDescriptor {
37
37
  enabled: boolean
38
38
  }
39
39
 
40
+ /**
41
+ * The verdict of a live "check setup" probe against a source (mirrors
42
+ * `@cat-factory/contracts`). Unlike `available` (a passive row-exists flag) this
43
+ * is the result of actually authenticating + reading, so it distinguishes a
44
+ * configured-but-broken source from a working one.
45
+ */
46
+ export type TaskSourceDiagnosticStatus =
47
+ | 'ready'
48
+ | 'not_installed'
49
+ | 'not_connected'
50
+ | 'auth_failed'
51
+ | 'forbidden'
52
+ | 'unreachable'
53
+ | 'error'
54
+
55
+ export interface TaskSourceDiagnostic {
56
+ source: TaskSourceKind
57
+ ok: boolean
58
+ status: TaskSourceDiagnosticStatus
59
+ /** A one-line, actionable explanation shown verbatim in the panel. */
60
+ message: string
61
+ /** Optional extra context (account login, repo count, signed-in user). */
62
+ detail?: string | null
63
+ }
64
+
40
65
  /** A workspace's connection to a task source (never carries credentials). */
41
66
  export interface TaskConnection {
42
67
  source: TaskSourceKind
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
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",
@@ -1,91 +0,0 @@
1
- <script setup lang="ts">
2
- // Workspace settings: issue-tracker writeback. Two independent toggles that govern
3
- // whether the engine writes back to a task's linked tracker issue(s) as its PR
4
- // progresses — comment when the PR opens, and comment + close as resolved when it
5
- // merges. Each is overridable per task in the inspector. Persisted on the workspace
6
- // tracker settings (the selection + Jira project key are preserved on save).
7
- import { onMounted, ref, watch } from 'vue'
8
-
9
- const tracker = useTrackerStore()
10
- const toast = useToast()
11
-
12
- const commentOnPrOpen = ref(false)
13
- const resolveOnMerge = ref(false)
14
- const saving = ref(false)
15
-
16
- // Sync the local toggles from the store on mount (the tab renders when Workspace
17
- // settings opens) and whenever the stored settings change underneath.
18
- function hydrate() {
19
- commentOnPrOpen.value = tracker.settings.writebackCommentOnPrOpen
20
- resolveOnMerge.value = tracker.settings.writebackResolveOnMerge
21
- }
22
- onMounted(hydrate)
23
- watch(() => tracker.settings, hydrate, { deep: true })
24
-
25
- async function save() {
26
- saving.value = true
27
- try {
28
- // Preserve the tracker selection + Jira project key; only the writeback flags change.
29
- await tracker.save({
30
- tracker: tracker.settings.tracker,
31
- jiraProjectKey: tracker.settings.jiraProjectKey,
32
- writebackCommentOnPrOpen: commentOnPrOpen.value,
33
- writebackResolveOnMerge: resolveOnMerge.value,
34
- })
35
- toast.add({ title: 'Writeback settings saved', icon: 'i-lucide-check', color: 'success' })
36
- } catch (e) {
37
- toast.add({
38
- title: 'Could not save settings',
39
- description: e instanceof Error ? e.message : String(e),
40
- icon: 'i-lucide-triangle-alert',
41
- color: 'error',
42
- })
43
- } finally {
44
- saving.value = false
45
- }
46
- }
47
- </script>
48
-
49
- <template>
50
- <div class="space-y-4">
51
- <p class="text-xs text-slate-400">
52
- When a task is linked to a tracker issue (GitHub Issues or Jira), write back to it as the
53
- task's pull request progresses. Each toggle is the workspace default and can be overridden per
54
- task in the inspector. GitHub issues close natively; Jira issues transition to the first
55
- status in their <span class="text-slate-300">Done</span> category.
56
- </p>
57
-
58
- <label class="flex items-start gap-3 rounded-lg border border-slate-700 bg-slate-800/40 p-3">
59
- <USwitch v-model="commentOnPrOpen" />
60
- <span class="text-sm">
61
- <span class="block text-slate-200">Comment when a PR opens</span>
62
- <span class="block text-xs text-slate-500">
63
- Post a comment on the linked issue with the new pull request's link.
64
- </span>
65
- </span>
66
- </label>
67
-
68
- <label class="flex items-start gap-3 rounded-lg border border-slate-700 bg-slate-800/40 p-3">
69
- <USwitch v-model="resolveOnMerge" />
70
- <span class="text-sm">
71
- <span class="block text-slate-200">Close as resolved when a PR merges</span>
72
- <span class="block text-xs text-slate-500">
73
- Comment that the PR merged, then close / resolve the linked issue.
74
- </span>
75
- </span>
76
- </label>
77
-
78
- <div class="flex justify-end">
79
- <UButton
80
- color="primary"
81
- variant="soft"
82
- size="sm"
83
- icon="i-lucide-save"
84
- :loading="saving"
85
- @click="save"
86
- >
87
- Save
88
- </UButton>
89
- </div>
90
- </div>
91
- </template>