@cat-factory/app 0.47.10 → 0.48.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.
@@ -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
  }}
@@ -15,8 +15,10 @@ const toast = useToast()
15
15
  const { t } = useI18n()
16
16
 
17
17
  const slack = reactive({ clientId: '', clientSecret: '', redirectUrl: '' })
18
+ const linear = reactive({ clientId: '', clientSecret: '', redirectUrl: '' })
18
19
  const web = reactive({ braveApiKey: '', searxngUrl: '', searxngApiKey: '' })
19
20
  const savingSlack = ref(false)
21
+ const savingLinear = ref(false)
20
22
  const savingWeb = ref(false)
21
23
 
22
24
  const summary = computed(() => store.view?.summary ?? null)
@@ -200,6 +202,61 @@ async function clearSlack() {
200
202
  }
201
203
  }
202
204
 
205
+ async function saveLinear() {
206
+ if (!linear.clientId.trim() || !linear.clientSecret.trim() || !linear.redirectUrl.trim()) {
207
+ toast.add({ title: t('layout.accountDeployment.linear.validation'), color: 'error' })
208
+ return
209
+ }
210
+ savingLinear.value = true
211
+ try {
212
+ await store.save(props.accountId, {
213
+ secrets: {
214
+ linearOAuth: {
215
+ clientId: linear.clientId.trim(),
216
+ clientSecret: linear.clientSecret.trim(),
217
+ redirectUrl: linear.redirectUrl.trim(),
218
+ },
219
+ },
220
+ })
221
+ linear.clientId = ''
222
+ linear.clientSecret = ''
223
+ linear.redirectUrl = ''
224
+ toast.add({
225
+ title: t('layout.accountDeployment.linear.saved'),
226
+ icon: 'i-lucide-check',
227
+ color: 'success',
228
+ })
229
+ } catch (e) {
230
+ toast.add({
231
+ title: t('layout.accountDeployment.linear.saveFailed'),
232
+ description: e instanceof Error ? e.message : String(e),
233
+ color: 'error',
234
+ })
235
+ } finally {
236
+ savingLinear.value = false
237
+ }
238
+ }
239
+
240
+ async function clearLinear() {
241
+ savingLinear.value = true
242
+ try {
243
+ await store.save(props.accountId, { secrets: { linearOAuth: null } })
244
+ toast.add({
245
+ title: t('layout.accountDeployment.linear.cleared'),
246
+ icon: 'i-lucide-check',
247
+ color: 'success',
248
+ })
249
+ } catch (e) {
250
+ toast.add({
251
+ title: t('layout.accountDeployment.linear.clearFailed'),
252
+ description: e instanceof Error ? e.message : String(e),
253
+ color: 'error',
254
+ })
255
+ } finally {
256
+ savingLinear.value = false
257
+ }
258
+ }
259
+
203
260
  async function saveWeb() {
204
261
  const brave = web.braveApiKey.trim()
205
262
  const searxng = web.searxngUrl.trim()
@@ -329,6 +386,68 @@ async function clearWeb() {
329
386
  </div>
330
387
  </section>
331
388
 
389
+ <!-- Linear app OAuth -->
390
+ <section class="space-y-2 border-t border-slate-800 pt-6">
391
+ <div class="flex items-center gap-2">
392
+ <h4 class="text-sm font-semibold text-slate-200">
393
+ {{ t('layout.accountDeployment.linear.title') }}
394
+ </h4>
395
+ <UBadge
396
+ :color="summary?.linearOAuthConfigured ? 'success' : 'neutral'"
397
+ variant="subtle"
398
+ size="xs"
399
+ >
400
+ {{
401
+ summary?.linearOAuthConfigured
402
+ ? t('layout.accountDeployment.configured')
403
+ : t('layout.accountDeployment.notSet')
404
+ }}
405
+ </UBadge>
406
+ </div>
407
+ <p class="text-[11px] text-slate-400">
408
+ {{ t('layout.accountDeployment.linear.description') }}
409
+ </p>
410
+ <div class="grid grid-cols-1 gap-2 sm:grid-cols-3">
411
+ <UInput
412
+ v-model="linear.clientId"
413
+ :placeholder="t('layout.accountDeployment.linear.clientId')"
414
+ size="sm"
415
+ />
416
+ <UInput
417
+ v-model="linear.clientSecret"
418
+ type="password"
419
+ :placeholder="t('layout.accountDeployment.linear.clientSecret')"
420
+ size="sm"
421
+ />
422
+ <UInput
423
+ v-model="linear.redirectUrl"
424
+ :placeholder="t('layout.accountDeployment.linear.redirectUrl')"
425
+ size="sm"
426
+ />
427
+ </div>
428
+ <div class="flex gap-2">
429
+ <UButton
430
+ color="primary"
431
+ size="xs"
432
+ icon="i-lucide-save"
433
+ :loading="savingLinear"
434
+ @click="saveLinear"
435
+ >
436
+ {{ t('common.save') }}
437
+ </UButton>
438
+ <UButton
439
+ v-if="summary?.linearOAuthConfigured"
440
+ color="neutral"
441
+ variant="ghost"
442
+ size="xs"
443
+ :loading="savingLinear"
444
+ @click="clearLinear"
445
+ >
446
+ {{ t('layout.accountDeployment.clear') }}
447
+ </UButton>
448
+ </div>
449
+ </section>
450
+
332
451
  <!-- Web search keys -->
333
452
  <section class="space-y-2 border-t border-slate-800 pt-6">
334
453
  <div class="flex items-center gap-2">
@@ -68,6 +68,39 @@ const canSave = computed(() => {
68
68
  return true
69
69
  })
70
70
 
71
+ // Linear team picker: load the connected workspace's teams so filing offers a
72
+ // dropdown instead of a raw team-id paste. Falls back to the text input if the
73
+ // teams can't be loaded (a broken connection shouldn't block configuration).
74
+ const teamsLoading = ref(false)
75
+ const teamsError = ref(false)
76
+ const teamOptions = computed(() =>
77
+ tracker.linearTeams.map((tm) => ({
78
+ label: tm.key ? `${tm.name} (${tm.key})` : tm.name,
79
+ value: tm.id,
80
+ })),
81
+ )
82
+ async function loadLinearTeams() {
83
+ if (!linearConnected.value) return
84
+ teamsLoading.value = true
85
+ teamsError.value = false
86
+ try {
87
+ await tracker.loadLinearTeams()
88
+ } catch {
89
+ teamsError.value = true
90
+ } finally {
91
+ teamsLoading.value = false
92
+ }
93
+ }
94
+ watch(
95
+ () => [trackerKind.value, linearConnected.value] as const,
96
+ ([kind, connected]) => {
97
+ if (kind === 'linear' && connected && tracker.linearTeams.length === 0 && !teamsError.value) {
98
+ void loadLinearTeams()
99
+ }
100
+ },
101
+ { immediate: true },
102
+ )
103
+
71
104
  async function save() {
72
105
  if (!canSave.value) return
73
106
  saving.value = true
@@ -284,7 +317,21 @@ const STATUS_UI: Record<
284
317
  :label="t('settings.issueTracker.filing.linearTeamId')"
285
318
  class="w-64"
286
319
  >
287
- <UInput v-model="linearTeamId" placeholder="team_…" size="sm" class="w-full" />
320
+ <!-- Typeahead combobox when the connection's teams loaded (built-in client-side
321
+ filter over the option labels — a large org's team list is too long for a
322
+ plain dropdown); raw-id fallback otherwise. Mirrors the repo picker. -->
323
+ <UInputMenu
324
+ v-if="linearConnected && !teamsError && teamOptions.length > 0"
325
+ v-model="linearTeamId"
326
+ :items="teamOptions"
327
+ value-key="value"
328
+ :loading="teamsLoading"
329
+ icon="i-lucide-search"
330
+ :placeholder="t('settings.issueTracker.filing.linearTeamSearchPlaceholder')"
331
+ size="sm"
332
+ class="w-full"
333
+ />
334
+ <UInput v-else v-model="linearTeamId" placeholder="team_…" size="sm" class="w-full" />
288
335
  <template #help>
289
336
  <span class="text-[11px] text-slate-500">
290
337
  {{ t('settings.issueTracker.filing.linearTeamIdHelp') }}
@@ -24,6 +24,9 @@ const connection = computed(() => (source.value ? tasks.connectionFor(source.val
24
24
  const connected = computed(() => connection.value !== undefined)
25
25
  // A credentialless source (GitHub Issues) reuses the installed GitHub App: no form.
26
26
  const credentialless = computed(() => (descriptor.value?.credentialFields.length ?? 0) === 0)
27
+ // An OAuth source (Linear) offers a "Connect with X" button alongside the manual fields.
28
+ const oauth = computed(() => descriptor.value?.oauth ?? false)
29
+ const oauthStarting = ref(false)
27
30
  // Usable right now: a credentialed source is connected; GitHub Issues' App is installed.
28
31
  const available = computed(() => descriptor.value?.available ?? false)
29
32
 
@@ -77,6 +80,23 @@ async function submit() {
77
80
  }
78
81
  }
79
82
 
83
+ async function startOAuth() {
84
+ if (!source.value) return
85
+ oauthStarting.value = true
86
+ try {
87
+ // Only Linear wires an OAuth flow today; the browser navigates away on success.
88
+ if (source.value === 'linear') await tasks.startLinearOAuth()
89
+ } catch (e) {
90
+ toast.add({
91
+ title: t('tasks.connect.connectFailed'),
92
+ description: e instanceof Error ? e.message : String(e),
93
+ icon: 'i-lucide-triangle-alert',
94
+ color: 'error',
95
+ })
96
+ oauthStarting.value = false
97
+ }
98
+ }
99
+
80
100
  async function disconnect() {
81
101
  if (!source.value) return
82
102
  await tasks.disconnect(source.value)
@@ -129,8 +149,23 @@ async function toggleEnabled(enabled: boolean) {
129
149
  </p>
130
150
  </template>
131
151
 
132
- <!-- Credentialed source (Jira): the connect form, shown until connected. -->
152
+ <!-- Credentialed source (Jira/Linear): the connect form, shown until connected. -->
133
153
  <div v-else-if="!connected" class="space-y-3">
154
+ <!-- OAuth source (Linear): the redirect button, with the manual key form below. -->
155
+ <template v-if="oauth">
156
+ <UButton
157
+ block
158
+ color="primary"
159
+ icon="i-lucide-plug"
160
+ :loading="oauthStarting"
161
+ @click="startOAuth"
162
+ >
163
+ {{ t('tasks.connect.oauthButton', { label: descriptor.label }) }}
164
+ </UButton>
165
+ <p class="text-center text-[11px] text-slate-500">
166
+ {{ t('tasks.connect.oauthOr') }}
167
+ </p>
168
+ </template>
134
169
  <UFormField
135
170
  v-for="field in descriptor.credentialFields"
136
171
  :key="field.key"
@@ -3,9 +3,11 @@ import {
3
3
  createTaskFromIssueContract,
4
4
  diagnoseTaskSourceContract,
5
5
  disconnectTaskSourceContract,
6
+ getLinearInstallUrlContract,
6
7
  getTrackerSettingsContract,
7
8
  importTaskContract,
8
9
  linkTaskContract,
10
+ listLinearTeamsContract,
9
11
  listTaskConnectionsContract,
10
12
  listTaskSourcesContract,
11
13
  listTasksContract,
@@ -95,6 +97,15 @@ export function tasksApi({ send, ws }: ApiContext) {
95
97
  body: { ref: string; containerId: string; position?: { x: number; y: number } },
96
98
  ) => send(spawnEpicContract, { pathPrefix: ws(workspaceId), pathParams: { source }, body }),
97
99
 
100
+ // ---- Linear-specific --------------------------------------------------
101
+ // The connection's Linear teams, for the ticket-filing team picker.
102
+ listLinearTeams: (workspaceId: string) =>
103
+ send(listLinearTeamsContract, { pathPrefix: ws(workspaceId) }),
104
+
105
+ // The "Connect with Linear" OAuth authorize URL (the browser is redirected to it).
106
+ getLinearInstallUrl: (workspaceId: string) =>
107
+ send(getLinearInstallUrlContract, { pathPrefix: ws(workspaceId) }),
108
+
98
109
  // ---- issue-tracker selection (workspace-level) ------------------------
99
110
  getTrackerSettings: (workspaceId: string) =>
100
111
  send(getTrackerSettingsContract, { pathPrefix: ws(workspaceId) }),
@@ -90,6 +90,17 @@ export const useTasksStore = defineStore('tasks', () => {
90
90
  available.value = true
91
91
  }
92
92
 
93
+ /**
94
+ * Start the "Connect with Linear" OAuth flow by navigating the browser to the
95
+ * authorize URL the backend mints (carrying a signed `state`). Linear redirects
96
+ * back to the public callback, which stores the token; the settings panel's
97
+ * `probe()` on return then reflects the new connection.
98
+ */
99
+ async function startLinearOAuth() {
100
+ const { url } = await api.getLinearInstallUrl(workspace.requireId())
101
+ window.location.href = url
102
+ }
103
+
93
104
  /** Disconnect the workspace from a source. */
94
105
  async function disconnect(source: TaskSourceKind) {
95
106
  await api.disconnectTaskSource(workspace.requireId(), source)
@@ -203,6 +214,7 @@ export const useTasksStore = defineStore('tasks', () => {
203
214
  probe,
204
215
  checkSetup,
205
216
  connect,
217
+ startLinearOAuth,
206
218
  disconnect,
207
219
  setEnabled,
208
220
  loadTasks,
@@ -1,5 +1,6 @@
1
1
  import { defineStore } from 'pinia'
2
2
  import { ref } from 'vue'
3
+ import type { LinearTeam } from '~/types/domain'
3
4
  import type { PutTrackerSettingsInput, TrackerSettings } from '~/types/tracker'
4
5
  import { useWorkspaceStore } from '~/stores/workspace'
5
6
 
@@ -20,6 +21,9 @@ export const useTrackerStore = defineStore('tracker', () => {
20
21
  updatedAt: 0,
21
22
  })
22
23
 
24
+ /** The connected Linear workspace's teams, for the filing team picker (lazily loaded). */
25
+ const linearTeams = ref<LinearTeam[]>([])
26
+
23
27
  function hydrate(value: TrackerSettings | undefined) {
24
28
  settings.value = value ?? {
25
29
  tracker: null,
@@ -37,5 +41,12 @@ export const useTrackerStore = defineStore('tracker', () => {
37
41
  return settings.value
38
42
  }
39
43
 
40
- return { settings, hydrate, save }
44
+ /** Load the connected Linear workspace's teams for the filing team picker. */
45
+ async function loadLinearTeams() {
46
+ const ws = useWorkspaceStore()
47
+ const { teams } = await api.listLinearTeams(ws.requireId())
48
+ linearTeams.value = teams
49
+ }
50
+
51
+ return { settings, linearTeams, hydrate, save, loadLinearTeams }
41
52
  })
@@ -19,4 +19,5 @@ export type {
19
19
  SourceTask,
20
20
  TaskSearchResult,
21
21
  CredentialField,
22
+ LinearTeam,
22
23
  } from '@cat-factory/contracts'
@@ -828,6 +828,18 @@
828
828
  "cleared": "Slack OAuth cleared",
829
829
  "clearFailed": "Could not clear Slack OAuth"
830
830
  },
831
+ "linear": {
832
+ "title": "Linear app (OAuth)",
833
+ "description": "Enables the \"Connect with Linear\" OAuth flow. Without it, workspaces can still connect Linear by pasting a personal API key.",
834
+ "clientId": "Client ID",
835
+ "clientSecret": "Client secret",
836
+ "redirectUrl": "Redirect URL",
837
+ "validation": "Enter the client id, secret and redirect URL",
838
+ "saved": "Linear OAuth saved",
839
+ "saveFailed": "Could not save Linear OAuth",
840
+ "cleared": "Linear OAuth cleared",
841
+ "clearFailed": "Could not clear Linear OAuth"
842
+ },
831
843
  "web": {
832
844
  "title": "Container web search",
833
845
  "description": "The search upstream container agents reach through the backend proxy. Set a Brave key (recommended), or a self-hosted SearXNG URL (with an optional bearer key).",
@@ -1340,7 +1352,8 @@
1340
1352
  "jiraProjectKey": "Jira project key",
1341
1353
  "jiraProjectKeyHelp": "New tickets are filed under this project.",
1342
1354
  "linearTeamId": "Linear team id",
1343
- "linearTeamIdHelp": "New issues are created under this team (Linear requires a team to create an issue)."
1355
+ "linearTeamIdHelp": "New issues are created under this team (Linear requires a team to create an issue).",
1356
+ "linearTeamSearchPlaceholder": "Search teams…"
1344
1357
  },
1345
1358
  "vendor": {
1346
1359
  "github": "GitHub Issues",
@@ -1936,9 +1949,13 @@
1936
1949
  "repository": "Repository",
1937
1950
  "repositoryHint": "Repositories the GitHub App can access. Don't see yours? Grant the App access below, then refresh.",
1938
1951
  "noReposAvailable": "No repositories available yet. Grant the App access to one below, then refresh.",
1939
- "filterPlaceholder": "Filter by owner/name…",
1940
- "clearFilter": "Clear filter",
1941
- "chooseRepository": "Choose a repository",
1952
+ "searchPlaceholder": "Search repositories by owner or name…",
1953
+ "searchMinChars": "Type at least {min} character to search. | Type at least {min} characters to search.",
1954
+ "@searchMinChars": {
1955
+ "description": "Shown when a large repo list needs a typed query before searching. Resolved via t(key, { min }, min) so {min} also drives the plural choice; min is always 2 or more. Provide ALL plural forms your language needs (English has 2; Polish/Ukrainian need 3 - one/few/many - and rely on the custom pluralRules wired in i18n.config.ts)."
1956
+ },
1957
+ "noMatches": "No repositories found for {query}.",
1958
+ "clearSelection": "Clear selection",
1942
1959
  "showingCount": "Showing {shown} of {total} repositories.",
1943
1960
  "repoLabel": {
1944
1961
  "private": " (private)",
@@ -2164,7 +2181,9 @@
2164
2181
  "offerHint": "When off, {label} is hidden from import and linking.",
2165
2182
  "disconnect": "Disconnect",
2166
2183
  "updateConnection": "Update connection",
2167
- "connect": "Connect"
2184
+ "connect": "Connect",
2185
+ "oauthButton": "Connect with {label}",
2186
+ "oauthOr": "or connect with an API key"
2168
2187
  }
2169
2188
  },
2170
2189
  "pipeline": {
@@ -789,6 +789,18 @@
789
789
  "cleared": "OAuth de Slack borrado",
790
790
  "clearFailed": "No se pudo borrar el OAuth de Slack"
791
791
  },
792
+ "linear": {
793
+ "title": "Aplicación de Linear (OAuth)",
794
+ "description": "Habilita el flujo de OAuth \"Conectar con Linear\". Sin ello, los espacios de trabajo aún pueden conectar Linear pegando una clave de API personal.",
795
+ "clientId": "ID de cliente",
796
+ "clientSecret": "Secreto de cliente",
797
+ "redirectUrl": "URL de redirección",
798
+ "validation": "Introduce el id de cliente, el secreto y la URL de redirección",
799
+ "saved": "OAuth de Linear guardado",
800
+ "saveFailed": "No se pudo guardar OAuth de Linear",
801
+ "cleared": "OAuth de Linear borrado",
802
+ "clearFailed": "No se pudo borrar OAuth de Linear"
803
+ },
792
804
  "web": {
793
805
  "title": "Búsqueda web del contenedor",
794
806
  "description": "El proveedor de búsqueda al que los agentes del contenedor acceden a través del proxy del backend. Define una clave de Brave (recomendado) o una URL de SearXNG autoalojada (con una clave bearer opcional).",
@@ -1298,7 +1310,8 @@
1298
1310
  "jiraProjectKey": "Clave de proyecto de Jira",
1299
1311
  "jiraProjectKeyHelp": "Los nuevos tickets se registran en este proyecto.",
1300
1312
  "linearTeamId": "Id de equipo de Linear",
1301
- "linearTeamIdHelp": "Las nuevas incidencias se crean en este equipo (Linear requiere un equipo para crear una incidencia)."
1313
+ "linearTeamIdHelp": "Las nuevas incidencias se crean en este equipo (Linear requiere un equipo para crear una incidencia).",
1314
+ "linearTeamSearchPlaceholder": "Buscar equipos…"
1302
1315
  },
1303
1316
  "vendor": {
1304
1317
  "github": "GitHub Issues",
@@ -1885,9 +1898,10 @@
1885
1898
  "repository": "Repositorio",
1886
1899
  "repositoryHint": "Repositorios a los que la GitHub App puede acceder. ¿No ves el tuyo? Concede acceso a la App abajo y luego actualiza.",
1887
1900
  "noReposAvailable": "Aún no hay repositorios disponibles. Concede acceso a la App a uno abajo y luego actualiza.",
1888
- "filterPlaceholder": "Filtrar por propietario/nombre…",
1889
- "clearFilter": "Borrar filtro",
1890
- "chooseRepository": "Elige un repositorio",
1901
+ "searchPlaceholder": "Busca repositorios por propietario o nombre…",
1902
+ "searchMinChars": "Escribe al menos {min} carácter para buscar. | Escribe al menos {min} caracteres para buscar.",
1903
+ "noMatches": "No se encontraron repositorios para {query}.",
1904
+ "clearSelection": "Borrar selección",
1891
1905
  "showingCount": "Mostrando {shown} de {total} repositorios.",
1892
1906
  "repoLabel": {
1893
1907
  "private": " (privado)",
@@ -2110,7 +2124,9 @@
2110
2124
  "offerHint": "Cuando está desactivada, {label} se oculta de la importación y la vinculación.",
2111
2125
  "disconnect": "Desconectar",
2112
2126
  "updateConnection": "Actualizar conexión",
2113
- "connect": "Conectar"
2127
+ "connect": "Conectar",
2128
+ "oauthButton": "Conectar con {label}",
2129
+ "oauthOr": "o conecta con una clave API"
2114
2130
  }
2115
2131
  },
2116
2132
  "pipeline": {
@@ -789,6 +789,18 @@
789
789
  "cleared": "OAuth Slack effacé",
790
790
  "clearFailed": "Impossible d'effacer l'OAuth Slack"
791
791
  },
792
+ "linear": {
793
+ "title": "Application Linear (OAuth)",
794
+ "description": "Active le flux OAuth « Se connecter avec Linear ». Sans cela, les espaces de travail peuvent toujours connecter Linear en collant une clé API personnelle.",
795
+ "clientId": "ID client",
796
+ "clientSecret": "Secret client",
797
+ "redirectUrl": "URL de redirection",
798
+ "validation": "Saisissez l'id client, le secret et l'URL de redirection",
799
+ "saved": "OAuth Linear enregistré",
800
+ "saveFailed": "Impossible d'enregistrer OAuth Linear",
801
+ "cleared": "OAuth Linear effacé",
802
+ "clearFailed": "Impossible d'effacer OAuth Linear"
803
+ },
792
804
  "web": {
793
805
  "title": "Recherche web du conteneur",
794
806
  "description": "Le fournisseur de recherche que les agents en conteneur atteignent via le proxy du backend. Définissez une clé Brave (recommandé) ou une URL SearXNG auto-hébergée (avec une clé bearer facultative).",
@@ -1298,7 +1310,8 @@
1298
1310
  "jiraProjectKey": "Clé de projet Jira",
1299
1311
  "jiraProjectKeyHelp": "Les nouveaux tickets sont créés dans ce projet.",
1300
1312
  "linearTeamId": "Id d'équipe Linear",
1301
- "linearTeamIdHelp": "Les nouveaux tickets sont créés dans cette équipe (Linear exige une équipe pour créer un ticket)."
1313
+ "linearTeamIdHelp": "Les nouveaux tickets sont créés dans cette équipe (Linear exige une équipe pour créer un ticket).",
1314
+ "linearTeamSearchPlaceholder": "Rechercher des équipes…"
1302
1315
  },
1303
1316
  "vendor": {
1304
1317
  "github": "GitHub Issues",
@@ -1885,9 +1898,10 @@
1885
1898
  "repository": "Dépôt",
1886
1899
  "repositoryHint": "Dépôts auxquels la GitHub App peut accéder. Vous ne voyez pas le vôtre ? Accordez l'accès à l'App ci-dessous, puis actualisez.",
1887
1900
  "noReposAvailable": "Aucun dépôt disponible pour le moment. Accordez l'accès de l'App à l'un d'eux ci-dessous, puis actualisez.",
1888
- "filterPlaceholder": "Filtrer par propriétaire/nom…",
1889
- "clearFilter": "Effacer le filtre",
1890
- "chooseRepository": "Choisir un dépôt",
1901
+ "searchPlaceholder": "Rechercher des dépôts par propriétaire ou nom…",
1902
+ "searchMinChars": "Saisissez au moins {min} caractère pour rechercher. | Saisissez au moins {min} caractères pour rechercher.",
1903
+ "noMatches": "Aucun dépôt trouvé pour {query}.",
1904
+ "clearSelection": "Effacer la sélection",
1891
1905
  "showingCount": "Affichage de {shown} dépôts sur {total}.",
1892
1906
  "repoLabel": {
1893
1907
  "private": " (privé)",
@@ -2110,7 +2124,9 @@
2110
2124
  "offerHint": "Lorsqu'elle est désactivée, {label} est masquée de l'importation et de la liaison.",
2111
2125
  "disconnect": "Déconnecter",
2112
2126
  "updateConnection": "Mettre à jour la connexion",
2113
- "connect": "Connecter"
2127
+ "connect": "Connecter",
2128
+ "oauthButton": "Se connecter avec {label}",
2129
+ "oauthOr": "ou connectez-vous avec une clé API"
2114
2130
  }
2115
2131
  },
2116
2132
  "pipeline": {
@@ -789,6 +789,18 @@
789
789
  "cleared": "Wyczyszczono OAuth Slacka",
790
790
  "clearFailed": "Nie udało się wyczyścić OAuth Slacka"
791
791
  },
792
+ "linear": {
793
+ "title": "Aplikacja Linear (OAuth)",
794
+ "description": "Włącza przepływ OAuth „Połącz z Linear”. Bez niego przestrzenie robocze nadal mogą połączyć Linear, wklejając osobisty klucz API.",
795
+ "clientId": "Identyfikator klienta",
796
+ "clientSecret": "Sekret klienta",
797
+ "redirectUrl": "Adres URL przekierowania",
798
+ "validation": "Podaj identyfikator klienta, sekret i adres URL przekierowania",
799
+ "saved": "Zapisano OAuth Linear",
800
+ "saveFailed": "Nie udało się zapisać OAuth Linear",
801
+ "cleared": "Wyczyszczono OAuth Linear",
802
+ "clearFailed": "Nie udało się wyczyścić OAuth Linear"
803
+ },
792
804
  "web": {
793
805
  "title": "Wyszukiwanie w sieci w kontenerze",
794
806
  "description": "Dostawca wyszukiwania, do którego agenci w kontenerze sięgają przez proxy backendu. Ustaw klucz Brave (zalecane) lub własny adres URL SearXNG (z opcjonalnym kluczem bearer).",
@@ -1298,7 +1310,8 @@
1298
1310
  "jiraProjectKey": "Klucz projektu Jira",
1299
1311
  "jiraProjectKeyHelp": "Nowe zgłoszenia są rejestrowane w tym projekcie.",
1300
1312
  "linearTeamId": "Identyfikator zespołu Linear",
1301
- "linearTeamIdHelp": "Nowe zgłoszenia są tworzone w tym zespole (Linear wymaga zespołu do utworzenia zgłoszenia)."
1313
+ "linearTeamIdHelp": "Nowe zgłoszenia są tworzone w tym zespole (Linear wymaga zespołu do utworzenia zgłoszenia).",
1314
+ "linearTeamSearchPlaceholder": "Szukaj zespołów…"
1302
1315
  },
1303
1316
  "vendor": {
1304
1317
  "github": "GitHub Issues",
@@ -1885,9 +1898,10 @@
1885
1898
  "repository": "Repozytorium",
1886
1899
  "repositoryHint": "Repozytoria, do których aplikacja GitHub ma dostęp. Nie widzisz swojego? Przyznaj aplikacji dostęp poniżej, a następnie odśwież.",
1887
1900
  "noReposAvailable": "Brak dostępnych repozytoriów. Przyznaj aplikacji dostęp do jednego poniżej, a następnie odśwież.",
1888
- "filterPlaceholder": "Filtruj według właściciela/nazwy…",
1889
- "clearFilter": "Wyczyść filtr",
1890
- "chooseRepository": "Wybierz repozytorium",
1901
+ "searchPlaceholder": "Szukaj repozytoriów według właściciela lub nazwy…",
1902
+ "searchMinChars": "Wpisz co najmniej {min} znak, aby wyszukać. | Wpisz co najmniej {min} znaki, aby wyszukać. | Wpisz co najmniej {min} znaków, aby wyszukać.",
1903
+ "noMatches": "Nie znaleziono repozytoriów dla {query}.",
1904
+ "clearSelection": "Wyczyść wybór",
1891
1905
  "showingCount": "Wyświetlanie {shown} z {total} repozytoriów.",
1892
1906
  "repoLabel": {
1893
1907
  "private": " (prywatne)",
@@ -2110,7 +2124,9 @@
2110
2124
  "offerHint": "Po wyłączeniu {label} jest ukryte przed importem i powiązywaniem.",
2111
2125
  "disconnect": "Rozłącz",
2112
2126
  "updateConnection": "Zaktualizuj połączenie",
2113
- "connect": "Połącz"
2127
+ "connect": "Połącz",
2128
+ "oauthButton": "Połącz z {label}",
2129
+ "oauthOr": "lub połącz za pomocą klucza API"
2114
2130
  }
2115
2131
  },
2116
2132
  "pipeline": {
@@ -789,6 +789,18 @@
789
789
  "cleared": "OAuth Slack очищено",
790
790
  "clearFailed": "Не вдалося очистити OAuth Slack"
791
791
  },
792
+ "linear": {
793
+ "title": "Застосунок Linear (OAuth)",
794
+ "description": "Вмикає потік OAuth «Підключити через Linear». Без нього робочі простори все одно можуть підключити Linear, вставивши особистий ключ API.",
795
+ "clientId": "ID клієнта",
796
+ "clientSecret": "Секрет клієнта",
797
+ "redirectUrl": "URL перенаправлення",
798
+ "validation": "Введіть ID клієнта, секрет і URL перенаправлення",
799
+ "saved": "OAuth Linear збережено",
800
+ "saveFailed": "Не вдалося зберегти OAuth Linear",
801
+ "cleared": "OAuth Linear очищено",
802
+ "clearFailed": "Не вдалося очистити OAuth Linear"
803
+ },
792
804
  "web": {
793
805
  "title": "Вебпошук контейнера",
794
806
  "description": "Постачальник пошуку, до якого агенти в контейнері звертаються через проксі бекенду. Задайте ключ Brave (рекомендовано) або власний URL SearXNG (з необов'язковим ключем bearer).",
@@ -1298,7 +1310,8 @@
1298
1310
  "jiraProjectKey": "Ключ проєкту Jira",
1299
1311
  "jiraProjectKeyHelp": "Нові тикети реєструються в цьому проєкті.",
1300
1312
  "linearTeamId": "Ідентифікатор команди Linear",
1301
- "linearTeamIdHelp": "Нові тикети створюються в цій команді (Linear вимагає команду для створення тикета)."
1313
+ "linearTeamIdHelp": "Нові тикети створюються в цій команді (Linear вимагає команду для створення тикета).",
1314
+ "linearTeamSearchPlaceholder": "Пошук команд…"
1302
1315
  },
1303
1316
  "vendor": {
1304
1317
  "github": "GitHub Issues",
@@ -1885,9 +1898,10 @@
1885
1898
  "repository": "Репозиторій",
1886
1899
  "repositoryHint": "Репозиторії, до яких застосунок GitHub має доступ. Не бачите свого? Надайте застосунку доступ нижче, а потім оновіть.",
1887
1900
  "noReposAvailable": "Поки що немає доступних репозиторіїв. Надайте застосунку доступ до одного нижче, а потім оновіть.",
1888
- "filterPlaceholder": "Фільтрувати за власником/назвою…",
1889
- "clearFilter": "Очистити фільтр",
1890
- "chooseRepository": "Виберіть репозиторій",
1901
+ "searchPlaceholder": "Шукайте репозиторії за власником або назвою…",
1902
+ "searchMinChars": "Введіть щонайменше {min} символ для пошуку. | Введіть щонайменше {min} символи для пошуку. | Введіть щонайменше {min} символів для пошуку.",
1903
+ "noMatches": "Не знайдено репозиторіїв для {query}.",
1904
+ "clearSelection": "Очистити вибір",
1891
1905
  "showingCount": "Показано {shown} із {total} репозиторіїв.",
1892
1906
  "repoLabel": {
1893
1907
  "private": " (приватний)",
@@ -2110,7 +2124,9 @@
2110
2124
  "offerHint": "Коли вимкнено, {label} приховано з імпорту та прив'язування.",
2111
2125
  "disconnect": "Відключити",
2112
2126
  "updateConnection": "Оновити підключення",
2113
- "connect": "Підключити"
2127
+ "connect": "Підключити",
2128
+ "oauthButton": "Підключити через {label}",
2129
+ "oauthOr": "або підключіться за допомогою ключа API"
2114
2130
  }
2115
2131
  },
2116
2132
  "pipeline": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.47.10",
3
+ "version": "0.48.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",
@@ -34,7 +34,7 @@
34
34
  "valibot": "^1.4.2",
35
35
  "vue": "^3.5.39",
36
36
  "wretch": "^3.0.9",
37
- "@cat-factory/contracts": "0.45.1"
37
+ "@cat-factory/contracts": "0.46.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@toad-contracts/testing": "0.3.2",