@cat-factory/app 0.47.9 → 0.47.11

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.
@@ -11,6 +11,8 @@ const ui = useUiStore()
11
11
  const models = useModelsStore()
12
12
  const workspace = useWorkspaceStore()
13
13
 
14
+ const { t, n } = useI18n()
15
+
14
16
  onMounted(() => models.ensureLoaded(workspace.workspaceId ?? undefined))
15
17
 
16
18
  const block = computed<Block | undefined>(() =>
@@ -64,7 +66,7 @@ function openApprovalFor(approvalId: string) {
64
66
  size="sm"
65
67
  @click="close"
66
68
  >
67
- Board
69
+ {{ t('focus.board') }}
68
70
  </UButton>
69
71
  <UIcon name="i-lucide-chevron-right" class="h-4 w-4 text-slate-600" />
70
72
  <div
@@ -75,7 +77,9 @@ function openApprovalFor(approvalId: string) {
75
77
  </div>
76
78
  <div>
77
79
  <h1 class="text-lg font-semibold text-white">{{ block.title }}</h1>
78
- <div class="text-xs text-slate-500">{{ typeMeta.label }} · focus view</div>
80
+ <div class="text-xs text-slate-500">
81
+ {{ t('focus.typeSubtitle', { type: typeMeta.label }) }}
82
+ </div>
79
83
  </div>
80
84
  <UBadge :color="statusMeta.chip as any" variant="subtle" class="ml-2">
81
85
  {{ statusMeta.label }}
@@ -89,7 +93,7 @@ function openApprovalFor(approvalId: string) {
89
93
  icon="i-lucide-play"
90
94
  trailing-icon="i-lucide-chevron-down"
91
95
  >
92
- {{ instance ? 'Re-run pipeline' : 'Run pipeline' }}
96
+ {{ instance ? t('focus.rerunPipeline') : t('focus.runPipeline') }}
93
97
  </UButton>
94
98
  </UDropdownMenu>
95
99
  <UButton icon="i-lucide-x" color="neutral" variant="ghost" @click="close" />
@@ -104,7 +108,7 @@ function openApprovalFor(approvalId: string) {
104
108
  <div class="mb-4 flex items-center gap-2">
105
109
  <UIcon name="i-lucide-workflow" class="h-4 w-4 text-slate-500" />
106
110
  <h2 class="text-sm font-semibold uppercase tracking-wide text-slate-400">
107
- {{ instance ? instance.pipelineName : 'No pipeline running' }}
111
+ {{ instance ? instance.pipelineName : t('focus.noPipelineRunning') }}
108
112
  </h2>
109
113
  </div>
110
114
 
@@ -119,7 +123,7 @@ function openApprovalFor(approvalId: string) {
119
123
  v-else
120
124
  class="flex flex-1 items-center justify-center rounded-xl border border-dashed border-slate-700 text-sm text-slate-500"
121
125
  >
122
- Run a pipeline to visualize the agents working on this block.
126
+ {{ t('focus.emptyPipelineHint') }}
123
127
  </div>
124
128
  </section>
125
129
 
@@ -129,29 +133,29 @@ function openApprovalFor(approvalId: string) {
129
133
  >
130
134
  <div>
131
135
  <div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
132
- Description
136
+ {{ t('focus.description') }}
133
137
  </div>
134
138
  <p class="text-sm text-slate-300">{{ block.description }}</p>
135
139
  </div>
136
140
  <div v-if="instance">
137
141
  <div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
138
- Overall progress
142
+ {{ t('focus.overallProgress') }}
139
143
  </div>
140
144
  <UProgress :model-value="Math.round(block.progress * 100)" />
141
145
  <div class="mt-1 text-[11px] text-slate-400">
142
- {{ Math.round(block.progress * 100) }}%
146
+ {{ n(block.progress, 'percent') }}
143
147
  </div>
144
148
  </div>
145
149
  <div>
146
150
  <div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
147
- Dependencies
151
+ {{ t('focus.dependencies') }}
148
152
  </div>
149
153
  <div v-if="deps.length" class="flex flex-wrap gap-1">
150
154
  <UBadge v-for="d in deps" :key="d.id" color="neutral" variant="subtle" size="sm">
151
155
  {{ d.title }}
152
156
  </UBadge>
153
157
  </div>
154
- <div v-else class="text-[11px] text-slate-500">None</div>
158
+ <div v-else class="text-[11px] text-slate-500">{{ t('focus.noDependencies') }}</div>
155
159
  </div>
156
160
  </aside>
157
161
  </div>
@@ -17,6 +17,8 @@ const execution = useExecutionStore()
17
17
  const board = useBoardStore()
18
18
  const followUps = useFollowUpsStore()
19
19
 
20
+ const { t } = useI18n()
21
+
20
22
  const { open, blockId, instanceId, stepIndex, close } = useResultView('follow-ups')
21
23
 
22
24
  const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
@@ -57,15 +59,25 @@ async function onDismiss(item: FollowUpItem) {
57
59
  if (id) await followUps.dismissItem(id, item.id).catch(() => {})
58
60
  }
59
61
 
62
+ // Exhaustive map of the item status enum → label key (literal keys keep the typed-key
63
+ // drift guard live, vs a runtime-built `followUp.status.${status}`).
64
+ const STATUS_LABEL_KEYS: Record<FollowUpItem['status'], string> = {
65
+ pending: 'followUp.status.pending',
66
+ filed: 'followUp.status.filed',
67
+ queued: 'followUp.status.queued',
68
+ answered: 'followUp.status.answered',
69
+ dismissed: 'followUp.status.dismissed',
70
+ }
71
+
60
72
  const STATUS_META: Record<
61
73
  FollowUpItem['status'],
62
- { label: string; badge: 'neutral' | 'info' | 'success' | 'warning'; text: string }
74
+ { badge: 'neutral' | 'info' | 'success' | 'warning'; text: string }
63
75
  > = {
64
- pending: { label: 'Needs a decision', badge: 'warning', text: 'text-amber-300' },
65
- filed: { label: 'Filed as issue', badge: 'success', text: 'text-emerald-300' },
66
- queued: { label: 'Sent to Coder', badge: 'info', text: 'text-sky-300' },
67
- answered: { label: 'Answered', badge: 'info', text: 'text-sky-300' },
68
- dismissed: { label: 'Dismissed', badge: 'neutral', text: 'text-slate-400' },
76
+ pending: { badge: 'warning', text: 'text-amber-300' },
77
+ filed: { badge: 'success', text: 'text-emerald-300' },
78
+ queued: { badge: 'info', text: 'text-sky-300' },
79
+ answered: { badge: 'info', text: 'text-sky-300' },
80
+ dismissed: { badge: 'neutral', text: 'text-slate-400' },
69
81
  }
70
82
  </script>
71
83
 
@@ -88,15 +100,20 @@ const STATUS_META: Record<
88
100
  </span>
89
101
  <div class="min-w-0 flex-1">
90
102
  <h2 class="truncate text-sm font-semibold text-slate-100">
91
- {{ FOLLOW_UP_COMPANION_META.label }}{{ block ? ` — ${block.title}` : '' }}
103
+ {{
104
+ block ? t('followUp.titleWithBlock', { title: block.title }) : t('followUp.title')
105
+ }}
92
106
  </h2>
93
107
  <p class="truncate text-[11px] text-slate-400">
94
- Forward-looking follow-ups & questions the Coder surfaced. The pipeline continues once
95
- every item is decided.
108
+ {{ t('followUp.subtitle') }}
96
109
  </p>
97
110
  </div>
98
111
  <UBadge :color="pendingCount > 0 ? 'warning' : 'success'" variant="subtle" size="sm">
99
- {{ pendingCount > 0 ? `${pendingCount} to decide` : 'All decided' }}
112
+ {{
113
+ pendingCount > 0
114
+ ? t('followUp.badge.toDecide', { count: pendingCount }, pendingCount)
115
+ : t('followUp.badge.allDecided')
116
+ }}
100
117
  </UBadge>
101
118
  <button
102
119
  class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
@@ -113,10 +130,9 @@ const STATUS_META: Record<
113
130
  class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
114
131
  >
115
132
  <UIcon :name="FOLLOW_UP_COMPANION_META.icon" class="h-8 w-8 opacity-40" />
116
- <p class="text-sm">No follow-ups yet.</p>
133
+ <p class="text-sm">{{ t('followUp.empty.title') }}</p>
117
134
  <p class="max-w-sm text-[11px] text-slate-500">
118
- As the Coder works it streams loose ends, side-tasks and questions here. They appear
119
- live — you can act on them before the Coder even finishes.
135
+ {{ t('followUp.empty.hint') }}
120
136
  </p>
121
137
  </div>
122
138
 
@@ -146,14 +162,15 @@ const STATUS_META: Record<
146
162
  {{ item.title }}
147
163
  </h3>
148
164
  <UBadge :color="STATUS_META[item.status].badge" variant="subtle" size="sm">
149
- {{ STATUS_META[item.status].label }}
165
+ {{ t(STATUS_LABEL_KEYS[item.status]) }}
150
166
  </UBadge>
151
167
  </div>
152
168
  <p v-if="item.detail" class="mt-1 whitespace-pre-wrap text-[12px] text-slate-300">
153
169
  {{ item.detail }}
154
170
  </p>
155
171
  <p v-if="item.suggestedAction" class="mt-1 text-[11px] text-slate-400">
156
- <span class="text-slate-500">Suggested:</span> {{ item.suggestedAction }}
172
+ <span class="text-slate-500">{{ t('followUp.suggested') }}</span>
173
+ {{ item.suggestedAction }}
157
174
  </p>
158
175
  <p v-if="item.status === 'filed' && item.ticketUrl" class="mt-1 text-[11px]">
159
176
  <a
@@ -162,14 +179,15 @@ const STATUS_META: Record<
162
179
  rel="noopener"
163
180
  class="text-emerald-300 hover:underline"
164
181
  >
165
- {{ item.ticketExternalId ?? 'View issue' }}
182
+ {{ item.ticketExternalId ?? t('followUp.viewIssue') }}
166
183
  </a>
167
184
  </p>
168
185
  <p
169
186
  v-if="item.status === 'answered' && item.answer"
170
187
  class="mt-1 text-[11px] text-slate-300"
171
188
  >
172
- <span class="text-slate-500">Your answer:</span> {{ item.answer }}
189
+ <span class="text-slate-500">{{ t('followUp.yourAnswer') }}</span>
190
+ {{ item.answer }}
173
191
  </p>
174
192
 
175
193
  <!-- Actions (only while the item is still undecided) -->
@@ -179,7 +197,7 @@ const STATUS_META: Record<
179
197
  <textarea
180
198
  v-model="drafts[item.id]"
181
199
  rows="2"
182
- placeholder="Answer this question — it's folded into the Coder's next pass…"
200
+ :placeholder="t('followUp.answerPlaceholder')"
183
201
  class="w-full resize-y rounded-md border border-slate-700 bg-slate-950/60 px-2.5 py-1.5 text-[12px] text-slate-100 placeholder:text-slate-600 focus:border-sky-500 focus:outline-none"
184
202
  />
185
203
  <div class="flex items-center gap-2">
@@ -190,7 +208,7 @@ const STATUS_META: Record<
190
208
  :disabled="!(drafts[item.id] ?? '').trim()"
191
209
  @click="onAnswer(item)"
192
210
  >
193
- Answer & send back
211
+ {{ t('followUp.actions.answerAndSend') }}
194
212
  </UButton>
195
213
  <UButton
196
214
  size="xs"
@@ -199,7 +217,7 @@ const STATUS_META: Record<
199
217
  :loading="followUps.isActing(item.id)"
200
218
  @click="onDismiss(item)"
201
219
  >
202
- Dismiss
220
+ {{ t('followUp.actions.dismiss') }}
203
221
  </UButton>
204
222
  </div>
205
223
  </div>
@@ -213,7 +231,7 @@ const STATUS_META: Record<
213
231
  :loading="followUps.isActing(item.id)"
214
232
  @click="onFile(item)"
215
233
  >
216
- File as issue
234
+ {{ t('followUp.actions.fileAsIssue') }}
217
235
  </UButton>
218
236
  <UButton
219
237
  size="xs"
@@ -223,7 +241,7 @@ const STATUS_META: Record<
223
241
  :loading="followUps.isActing(item.id)"
224
242
  @click="onQueue(item)"
225
243
  >
226
- Send to Coder
244
+ {{ t('followUp.actions.sendToCoder') }}
227
245
  </UButton>
228
246
  <UButton
229
247
  size="xs"
@@ -232,7 +250,7 @@ const STATUS_META: Record<
232
250
  :loading="followUps.isActing(item.id)"
233
251
  @click="onDismiss(item)"
234
252
  >
235
- Dismiss
253
+ {{ t('followUp.actions.dismiss') }}
236
254
  </UButton>
237
255
  </div>
238
256
  </div>
@@ -246,10 +264,17 @@ const STATUS_META: Record<
246
264
  class="flex items-center justify-between border-t border-slate-800 px-5 py-2.5 text-[11px] text-slate-400"
247
265
  >
248
266
  <span>
249
- {{ items.length }} item{{ items.length === 1 ? '' : 's' }} ·
250
- {{ pendingCount }} undecided
267
+ {{
268
+ t(
269
+ 'followUp.footer.summary',
270
+ { count: items.length, undecided: pendingCount },
271
+ items.length,
272
+ )
273
+ }}
251
274
  </span>
252
- <span v-if="maxLoops > 0">Coder loops used: {{ loops }} / {{ maxLoops }}</span>
275
+ <span v-if="maxLoops > 0">{{
276
+ t('followUp.footer.loops', { loops, max: maxLoops })
277
+ }}</span>
253
278
  </footer>
254
279
  </div>
255
280
  </div>
@@ -10,6 +10,7 @@
10
10
  // pinned to a subdirectory. When the selected repo is a monorepo, the user
11
11
  // browses its tree and picks the service's directory before adding (and may add
12
12
  // more than one, a subset of the repo's services).
13
+ import { refDebounced } from '@vueuse/core'
13
14
  import GitHubConnect from '~/components/github/GitHubConnect.vue'
14
15
  import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
15
16
  import ServiceTestConfig from '~/components/panels/inspector/ServiceTestConfig.vue'
@@ -76,16 +77,53 @@ const repoItems = computed(() =>
76
77
  }),
77
78
  )
78
79
 
79
- // The PAT (or a wide App install) can expose hundreds of repos, too many for a plain
80
- // dropdown — filter by owner/name. The currently selected repo is always kept in the
81
- // list so a selection doesn't vanish when the query no longer matches it.
80
+ // The PAT (or a wide App install) can expose hundreds of repos, far too many for a plain
81
+ // dropdown — so the picker is a typeahead combobox. The user types and matching repos
82
+ // surface: matching is a debounced, case-insensitive substring over `owner/name` (so any
83
+ // part of either matches). For a LARGE list the search only kicks in once at least
84
+ // MIN_SEARCH_LEN characters are typed, to keep early keystrokes from listing hundreds of
85
+ // rows; but when the whole list is small enough to browse (<= BROWSE_ALL_MAX) the gate is
86
+ // dropped and every repo is offered up-front (typing then narrows), so a handful of repos
87
+ // stays pickable without having to type — matching the old always-open dropdown.
88
+ const MIN_SEARCH_LEN = 3
89
+ const BROWSE_ALL_MAX = 25
82
90
  const repoSearch = ref('')
83
- const filteredRepoItems = computed(() => {
84
- const q = repoSearch.value.trim().toLowerCase()
85
- if (!q) return repoItems.value
86
- return repoItems.value.filter((r) => r.search.includes(q) || r.value === selectedRepoId.value)
91
+ const repoSearchDebounced = refDebounced(repoSearch, 250)
92
+ // Trimmed (original case) for display; lowercased for matching.
93
+ const repoQueryRaw = computed(() => repoSearchDebounced.value.trim())
94
+ const repoQuery = computed(() => repoQueryRaw.value.toLowerCase())
95
+
96
+ // Small lists are browseable without typing; large lists require the min-length gate.
97
+ const browseAll = computed(() => repoItems.value.length <= BROWSE_ALL_MAX)
98
+ // True only on a large list whose query is still too short to search.
99
+ const belowMinChars = computed(() => !browseAll.value && repoQuery.value.length < MIN_SEARCH_LEN)
100
+
101
+ // Matches for the current query. On a small list an empty query lists everything; on a
102
+ // large list nothing surfaces until the query passes the min length.
103
+ const queryMatches = computed(() => {
104
+ if (belowMinChars.value) return []
105
+ if (!repoQuery.value) return repoItems.value
106
+ return repoItems.value.filter((r) => r.search.includes(repoQuery.value))
107
+ })
108
+
109
+ // Items fed to the combobox: the query matches plus the current selection kept present,
110
+ // so the menu can still render the selected repo's label once the (reset) search term no
111
+ // longer matches it.
112
+ const repoMenuItems = computed(() => {
113
+ const matches = queryMatches.value
114
+ if (selectedRepoId.value === undefined) return matches
115
+ if (matches.some((r) => r.value === selectedRepoId.value)) return matches
116
+ const selected = repoItems.value.find((r) => r.value === selectedRepoId.value)
117
+ return selected ? [selected, ...matches] : matches
87
118
  })
88
119
 
120
+ // The count summary under the field is shown only when it's meaningful: there are matches
121
+ // to count AND the user is actually searching (or browsing a small list). After a
122
+ // selection resets the search term this is false, so the field doesn't claim "Showing 0"
123
+ // or nag "type 3 characters" right under the repo the user just picked. The zero-match and
124
+ // min-length messages are owned by the combobox's own empty state instead.
125
+ const showResultCount = computed(() => !belowMinChars.value && queryMatches.value.length > 0)
126
+
89
127
  const hasRepos = computed(() => github.availableRepos.length > 0)
90
128
  const selectedRepo = computed(() =>
91
129
  github.availableRepos.find((r) => r.githubId === selectedRepoId.value),
@@ -121,6 +159,13 @@ function resetSelection() {
121
159
  repoSearch.value = ''
122
160
  }
123
161
 
162
+ // Clear the current repo selection (the combobox's trailing ✕) so the user can pick a
163
+ // different one — drops the selection-dependent state and resets the search term. The
164
+ // combobox has no built-in deselect, so the field would otherwise stay pinned to a repo.
165
+ function clearSelection() {
166
+ resetSelection()
167
+ }
168
+
124
169
  // The App's installation settings page — where the user grants it access to a
125
170
  // repo it can't see yet (mirrors the bootstrap modal's "grant access" link).
126
171
  const manageInstallUrl = computed(() => {
@@ -236,34 +281,42 @@ function done() {
236
281
  {{ t('github.addService.noReposAvailable') }}
237
282
  </div>
238
283
  <div v-else class="space-y-1.5">
239
- <UInput
240
- v-model="repoSearch"
284
+ <UInputMenu
285
+ v-model="selectedRepoId"
286
+ v-model:search-term="repoSearch"
287
+ :items="repoMenuItems"
288
+ :ignore-filter="true"
289
+ value-key="value"
290
+ :loading="github.loadingAvailable"
241
291
  icon="i-lucide-search"
242
- :placeholder="t('github.addService.filterPlaceholder')"
292
+ :placeholder="t('github.addService.searchPlaceholder')"
243
293
  class="w-full"
244
- :ui="{ trailing: 'pe-1' }"
245
294
  >
246
- <template v-if="repoSearch" #trailing>
295
+ <template v-if="selectedRepoId !== undefined" #trailing>
247
296
  <UButton
248
297
  color="neutral"
249
298
  variant="link"
250
299
  size="sm"
251
300
  icon="i-lucide-x"
252
- :aria-label="t('github.addService.clearFilter')"
253
- @click="repoSearch = ''"
301
+ :aria-label="t('github.addService.clearSelection')"
302
+ @click.stop="clearSelection"
254
303
  />
255
304
  </template>
256
- </UInput>
257
- <USelect
258
- v-model="selectedRepoId"
259
- :items="filteredRepoItems"
260
- :placeholder="t('github.addService.chooseRepository')"
261
- class="w-full"
262
- />
263
- <p class="text-xs text-slate-500">
305
+ <template #empty>
306
+ <span v-if="belowMinChars">
307
+ {{
308
+ t('github.addService.searchMinChars', { min: MIN_SEARCH_LEN }, MIN_SEARCH_LEN)
309
+ }}
310
+ </span>
311
+ <span v-else>{{
312
+ t('github.addService.noMatches', { query: repoQueryRaw })
313
+ }}</span>
314
+ </template>
315
+ </UInputMenu>
316
+ <p v-if="showResultCount" class="text-xs text-slate-500">
264
317
  {{
265
318
  t('github.addService.showingCount', {
266
- shown: filteredRepoItems.length,
319
+ shown: queryMatches.length,
267
320
  total: repoItems.length,
268
321
  })
269
322
  }}