@cat-factory/app 0.26.2 → 0.26.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.
@@ -343,8 +343,11 @@ const ITEM_ICON: Record<string, string> = {
343
343
  <div class="space-y-3 p-4">
344
344
  <!-- frame header (doubles as the drag handle for the expanded frame) -->
345
345
  <div class="flex items-start justify-between gap-2">
346
+ <!-- `nopan` stops Vue Flow's pane from panning on a left-drag that starts on
347
+ this handle (it pans via d3-zoom's mousedown, which our pointerdown
348
+ stopPropagation can't intercept), so the grab drives the frame move. -->
346
349
  <div
347
- class="flex cursor-grab items-center gap-2 active:cursor-grabbing"
350
+ class="nopan flex cursor-grab items-center gap-2 active:cursor-grabbing"
348
351
  title="Drag service"
349
352
  @pointerdown="onFrameHandle"
350
353
  >
@@ -428,19 +431,21 @@ const ITEM_ICON: Record<string, string> = {
428
431
  <UIcon name="i-lucide-plus" class="h-3.5 w-3.5" /> Add the first task
429
432
  </button>
430
433
 
431
- <!-- resize handles (drag the borders to resize the service, Miro-style) -->
434
+ <!-- resize handles (drag the borders to resize the service, Miro-style).
435
+ `nopan` (alongside `nodrag`) so the pane doesn't pan while resizing —
436
+ same reason as the header handle above. -->
432
437
  <div
433
- class="nodrag absolute right-0 top-0 h-full w-2 cursor-ew-resize hover:bg-sky-400/20"
438
+ class="nodrag nopan absolute right-0 top-0 h-full w-2 cursor-ew-resize hover:bg-sky-400/20"
434
439
  title="Drag to resize"
435
440
  @pointerdown="onResize($event, 'e')"
436
441
  />
437
442
  <div
438
- class="nodrag absolute bottom-0 left-0 h-2 w-full cursor-ns-resize hover:bg-sky-400/20"
443
+ class="nodrag nopan absolute bottom-0 left-0 h-2 w-full cursor-ns-resize hover:bg-sky-400/20"
439
444
  title="Drag to resize"
440
445
  @pointerdown="onResize($event, 's')"
441
446
  />
442
447
  <div
443
- class="nodrag absolute bottom-0 right-0 h-4 w-4 cursor-nwse-resize"
448
+ class="nodrag nopan absolute bottom-0 right-0 h-4 w-4 cursor-nwse-resize"
444
449
  title="Drag to resize"
445
450
  @pointerdown="onResize($event, 'se')"
446
451
  >
@@ -34,9 +34,9 @@ function onHandle(e: PointerEvent) {
34
34
  pointerEvents: draggingId === taskId ? 'none' : undefined,
35
35
  }"
36
36
  >
37
- <!-- drag handle -->
37
+ <!-- drag handle (`nopan` so the pane doesn't pan on a left-drag from here) -->
38
38
  <div
39
- class="nodrag flex cursor-grab items-center justify-center rounded-t-lg border border-b-0 border-slate-700 bg-slate-800/80 py-px active:cursor-grabbing"
39
+ class="nodrag nopan flex cursor-grab items-center justify-center rounded-t-lg border border-b-0 border-slate-700 bg-slate-800/80 py-px active:cursor-grabbing"
40
40
  title="Drag task"
41
41
  @pointerdown="onHandle"
42
42
  >
@@ -48,9 +48,9 @@ function onResize(e: PointerEvent, edge: 'e' | 's' | 'se') {
48
48
  zIndex: draggingId === moduleId ? 50 : 5,
49
49
  }"
50
50
  >
51
- <!-- module header / drag handle -->
51
+ <!-- module header / drag handle (`nopan` so a left-drag moves it, not the pane) -->
52
52
  <div
53
- class="nodrag flex h-[30px] cursor-grab items-center gap-1 rounded-t-xl bg-violet-500/15 px-2 active:cursor-grabbing"
53
+ class="nodrag nopan flex h-[30px] cursor-grab items-center gap-1 rounded-t-xl bg-violet-500/15 px-2 active:cursor-grabbing"
54
54
  @pointerdown="onHandle"
55
55
  @click.stop="ui.select(moduleId)"
56
56
  >
@@ -73,19 +73,20 @@ function onResize(e: PointerEvent, edge: 'e' | 's' | 'se') {
73
73
  <DraggableTask v-for="t in tasks" :key="t.id" :task-id="t.id" />
74
74
  </div>
75
75
 
76
- <!-- resize handles (drag the borders to resize the module, Miro-style) -->
76
+ <!-- resize handles (drag the borders to resize the module, Miro-style).
77
+ `nopan` (with `nodrag`) so resizing doesn't pan the pane. -->
77
78
  <div
78
- class="nodrag absolute right-0 top-0 h-full w-2 cursor-ew-resize hover:bg-violet-400/20"
79
+ class="nodrag nopan absolute right-0 top-0 h-full w-2 cursor-ew-resize hover:bg-violet-400/20"
79
80
  title="Drag to resize"
80
81
  @pointerdown="onResize($event, 'e')"
81
82
  />
82
83
  <div
83
- class="nodrag absolute bottom-0 left-0 h-2 w-full cursor-ns-resize hover:bg-violet-400/20"
84
+ class="nodrag nopan absolute bottom-0 left-0 h-2 w-full cursor-ns-resize hover:bg-violet-400/20"
84
85
  title="Drag to resize"
85
86
  @pointerdown="onResize($event, 's')"
86
87
  />
87
88
  <div
88
- class="nodrag absolute bottom-0 right-0 h-4 w-4 cursor-nwse-resize"
89
+ class="nodrag nopan absolute bottom-0 right-0 h-4 w-4 cursor-nwse-resize"
89
90
  title="Drag to resize"
90
91
  @pointerdown="onResize($event, 'se')"
91
92
  >
@@ -30,6 +30,7 @@ const RECOMMENDED_SLUGS = [
30
30
  'openai/gpt-5.5',
31
31
  'google/gemini-3-pro',
32
32
  'deepseek/deepseek-chat',
33
+ 'moonshotai/kimi-k2.7-code',
33
34
  ]
34
35
 
35
36
  // Whether the workspace/user has an OpenRouter key connected at any reachable scope.
@@ -105,18 +106,35 @@ async function connectKey() {
105
106
  if (!keyValue.value.trim() || !workspace.workspaceId) return
106
107
  connectingKey.value = true
107
108
  try {
109
+ const scope = keyScope.value
108
110
  const input = {
109
111
  provider: 'openrouter' as const,
110
112
  label: keyLabel.value.trim() || 'openrouter key',
111
113
  key: keyValue.value.trim(),
112
114
  }
113
- if (keyScope.value === 'workspace') await apiKeys.addWorkspaceKey(input)
114
- else await apiKeys.addUserKey(input)
115
+ // The save endpoint stores the key WITHOUT validating it, so a wrong/expired key
116
+ // would otherwise be reported as "connected". Probe OpenRouter with the freshly
117
+ // stored key and only announce success when it's actually reachable; on rejection
118
+ // roll the key back so `keyConnected` stays false and the form remains for a retry.
119
+ const created =
120
+ scope === 'workspace' ? await apiKeys.addWorkspaceKey(input) : await apiKeys.addUserKey(input)
121
+ const result = await store.refresh(workspace.workspaceId)
122
+ if (!result.reachable) {
123
+ if (created) {
124
+ if (scope === 'workspace') await apiKeys.removeWorkspaceKey(created.id).catch(() => {})
125
+ else await apiKeys.removeUserKey(created.id).catch(() => {})
126
+ }
127
+ toast.add({
128
+ title: 'Could not connect key',
129
+ description: store.refreshError ?? 'OpenRouter rejected the key.',
130
+ icon: 'i-lucide-triangle-alert',
131
+ color: 'error',
132
+ })
133
+ return
134
+ }
115
135
  keyValue.value = ''
116
136
  keyLabel.value = ''
117
137
  toast.add({ title: 'OpenRouter key connected', icon: 'i-lucide-check', color: 'success' })
118
- // Now that a key exists, load the live catalog automatically.
119
- await refresh()
120
138
  } catch (e) {
121
139
  toast.add({
122
140
  title: 'Could not connect key',
@@ -39,6 +39,7 @@ export const useApiKeysStore = defineStore('apiKeys', () => {
39
39
  if (!workspaceId.value) return
40
40
  const created = await api.addWorkspaceApiKey(workspaceId.value, input)
41
41
  workspaceKeys.value = [...workspaceKeys.value, created]
42
+ return created
42
43
  }
43
44
 
44
45
  async function removeWorkspaceKey(id: string) {
@@ -50,6 +51,7 @@ export const useApiKeysStore = defineStore('apiKeys', () => {
50
51
  async function addUserKey(input: AddApiKeyInput) {
51
52
  const created = await api.addMyApiKey(input)
52
53
  userKeys.value = [...userKeys.value, created]
54
+ return created
53
55
  }
54
56
 
55
57
  async function removeUserKey(id: string) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.26.2",
3
+ "version": "0.26.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",