@cat-factory/app 0.315.3 → 0.315.6

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.
Files changed (79) hide show
  1. package/README.md +25 -4
  2. package/app/components/assistant/AssistantModal.vue +3 -2
  3. package/app/components/board/AddTaskModal.vue +3 -2
  4. package/app/components/board/CreateInitiativeModal.vue +3 -2
  5. package/app/components/board/RecurringPipelineModal.vue +3 -2
  6. package/app/components/board/ReviewFrictionDialog.vue +4 -3
  7. package/app/components/bootstrap/AdoptionReviewModal.vue +2 -0
  8. package/app/components/bootstrap/BootstrapModal.vue +4 -3
  9. package/app/components/common/AppModal.vue +50 -0
  10. package/app/components/common/ConfirmDialog.vue +5 -3
  11. package/app/components/common/KeyboardShortcutsHelp.vue +6 -4
  12. package/app/components/documents/DocumentImportModal.vue +3 -2
  13. package/app/components/documents/DocumentSourceConnectModal.vue +7 -2
  14. package/app/components/documents/DocumentTemplatesModal.vue +3 -2
  15. package/app/components/documents/SpawnPreviewModal.vue +3 -2
  16. package/app/components/documents/StartFromDesignModal.vue +3 -2
  17. package/app/components/environments/EnvironmentSetupWizard.vue +2 -0
  18. package/app/components/foundational/FoundationalServicePanel.vue +2 -0
  19. package/app/components/fragments/FragmentLibraryPanel.vue +2 -0
  20. package/app/components/github/AddServiceFromRepoModal.vue +5 -4
  21. package/app/components/github/GitHubPanel.vue +4 -3
  22. package/app/components/layout/BoardSwitcher.vue +7 -2
  23. package/app/components/layout/CommandBar.vue +3 -2
  24. package/app/components/layout/IntegrationsHub.vue +3 -6
  25. package/app/components/layout/ModelProvidersHub.vue +3 -6
  26. package/app/components/layout/PersonalSetupModal.vue +3 -6
  27. package/app/components/layout/RolePrompt.vue +3 -2
  28. package/app/components/notifications/NotificationSettingsPanel.vue +3 -6
  29. package/app/components/panels/DecisionModal.vue +3 -2
  30. package/app/components/panels/InspectorPanel.vue +6 -1
  31. package/app/components/panels/inspector/ServiceTestConfig.vue +6 -4
  32. package/app/components/pipeline/AgentPromptEditor.vue +2 -0
  33. package/app/components/pipeline/PipelineBuilder.vue +11 -2
  34. package/app/components/pipeline/PipelineHealthModal.vue +3 -2
  35. package/app/components/providers/AiPresetMismatchDialog.vue +3 -6
  36. package/app/components/providers/AiProviderOnboardingModal.vue +3 -6
  37. package/app/components/providers/PersonalCredentialModal.vue +3 -2
  38. package/app/components/providers/VendorCredentialsModal.vue +3 -6
  39. package/app/components/queue/DescribeWorkModal.vue +3 -2
  40. package/app/components/sandbox/SandboxPanel.vue +2 -0
  41. package/app/components/settings/AccountSettingsPanel.vue +1 -0
  42. package/app/components/settings/ApiTokensPanel.vue +3 -2
  43. package/app/components/settings/InfrastructureWindow.vue +3 -6
  44. package/app/components/settings/LocalModelEndpointsPanel.vue +3 -6
  45. package/app/components/settings/ModelConfigurationPanel.vue +9 -1
  46. package/app/components/settings/ModelPresetHealthModal.vue +3 -2
  47. package/app/components/settings/ObservabilityConnectionPanel.vue +3 -6
  48. package/app/components/settings/OpenRouterCatalogPanel.vue +3 -2
  49. package/app/components/settings/RiskPolicyHealthModal.vue +3 -2
  50. package/app/components/settings/UserSecretsSection.vue +3 -6
  51. package/app/components/settings/WorkspaceSettingsPanel.vue +1 -0
  52. package/app/components/setup/SetupChecklistPanel.vue +3 -2
  53. package/app/components/slack/SlackPanel.vue +3 -2
  54. package/app/components/tasks/BugHuntModal.vue +2 -0
  55. package/app/components/tasks/TaskImportModal.vue +3 -2
  56. package/app/components/tasks/TaskSourceConnectModal.vue +3 -2
  57. package/app/components/theme/ThemeImportModal.vue +4 -2
  58. package/app/components/tutorial/TutorialCatalogue.vue +4 -3
  59. package/app/components/tutorial/TutorialPrompt.vue +3 -2
  60. package/app/stores/github/connection.ts +2 -4
  61. package/app/stores/github/context.ts +5 -1
  62. package/app/stores/github/projectionLoad.ts +127 -0
  63. package/app/stores/github/repoActions.ts +7 -2
  64. package/app/stores/github.spec.ts +355 -1
  65. package/app/stores/github.ts +21 -22
  66. package/app/stores/ui/builderModelConfigHandoff.spec.ts +88 -0
  67. package/app/stores/ui/modals.ts +78 -12
  68. package/app/utils/overlaySize.ts +22 -0
  69. package/i18n/locales/de.json +1 -0
  70. package/i18n/locales/en.json +1 -0
  71. package/i18n/locales/es.json +1 -0
  72. package/i18n/locales/fr.json +1 -0
  73. package/i18n/locales/he.json +1 -0
  74. package/i18n/locales/it.json +1 -0
  75. package/i18n/locales/ja.json +1 -0
  76. package/i18n/locales/pl.json +1 -0
  77. package/i18n/locales/tr.json +1 -0
  78. package/i18n/locales/uk.json +1 -0
  79. package/package.json +1 -1
package/README.md CHANGED
@@ -34,7 +34,8 @@ A spatial planning surface. You lay out a system as a **board** of frames
34
34
  (services), modules and tasks on a [Vue Flow](https://vueflow.dev) canvas, wire up
35
35
  dependencies, attach requirements, and apply **agent pipelines** to blocks.
36
36
  Execution streams back in real time (step/subtask progress bars, decision
37
- prompts, failures with retry) so the canvas doubles as a live dashboard.
37
+ prompts, failures with retry) so the canvas doubles as a live dashboard. The board's second view,
38
+ the [queue](#queue-view), lists the same tasks by what each one needs.
38
39
 
39
40
  It is a thin client: there is **no business logic here**. Every mutation calls the
40
41
  Worker API and the stores hydrate from server snapshots and live updates pushed
@@ -43,8 +44,8 @@ over the WebSocket. How that sync works is written up in
43
44
 
44
45
  ## Tech stack
45
46
 
46
- - **Nuxt 4 / Vue 3** SPA: the board is `pages/index.vue`, beside three standalone routes
47
- (`mcp-authorize.vue`, `mcp-oauth-callback.vue`, `reset-password.vue`).
47
+ - **Nuxt 4 / Vue 3** SPA: the board and its queue view are `pages/index.vue`, beside three
48
+ standalone routes (`mcp-authorize.vue`, `mcp-oauth-callback.vue`, `reset-password.vue`).
48
49
  - **Pinia** (+ `pinia-plugin-persistedstate`): feature stores.
49
50
  - **Vue Flow** (`core`, `background`, `controls`, `node-resizer`): the canvas.
50
51
  - **Nuxt UI** + Tailwind: components and styling.
@@ -389,6 +390,18 @@ A phone's notch and home indicator cover whatever sits at the screen edge, and o
389
390
 
390
391
  `scripts/check-frontend-safe-area.mjs` (CI) refuses any other `fixed` surface that spans a whole side of the viewport, in `.vue`, `.ts` and `.css`. A surface with no edge content (a scrim, a drawing layer) or one that pads its own edges (the sidebar drawer, the inspector's bottom sheet) says why with a `safe-area-ok:` comment, on the line or the one above it, or above the element's opening tag.
391
392
 
393
+ ### Modals open at a named width
394
+
395
+ A modal states what KIND of content it holds, and the width follows from that in one place. Before this, each modal picked its own `max-w-*`: five widths for three kinds of content, and no rule for which one a new modal takes.
396
+
397
+ - **`<AppModal size="prompt">`** (448px): a confirmation, a decision, a short read such as the keyboard help, or a dialog with one or two fields.
398
+ - **`<AppModal size="form">`** (576px): a form, a list or a hub to work through.
399
+ - **`<AppModal size="wide">`** (672px): cards that each carry a paragraph and an action beside it, a row of tabs over a form, or three pickers side by side (the AI provider onboarding, the vendor credentials, the setup checklist, describe new work).
400
+ - [`common/AppModal.vue`](./app/components/common/AppModal.vue) is `UModal` with the width applied through `ui.content`; every other prop, listener and slot passes through, and a call site still passes other `ui` slots and non-width content classes (a stacked dialog's `z-[70]`). The widths live in [`utils/overlaySize.ts`](./app/utils/overlaySize.ts). It is a wrapper rather than an `app.config.ts` variant because `UModal` has no `size` prop and passes only `transition`, `fullscreen`, `overlay` and `scrollable` into its variant function, so a `size` variant would never be selected.
401
+ - **An always-`fullscreen` modal has no width** and stays a plain `UModal`. A modal bound fullscreen only sometimes (`:fullscreen="isPhone"`) is centered everywhere else, so it takes an `AppModal` size, which `AppModal` drops while the modal is fullscreen.
402
+ - **A workbench surface keeps its own width for now**: the libraries, settings panels, bug hunt, adoption review and environment wizard at 768px, the sandbox and the agent prompt editor above that. Their shell is the open decision in [#2359](https://github.com/kibertoad/cat-factory/issues/2359), so a named width for them now would be dead work. Each says why in a `<!-- workbench-width-ok: … -->` comment directly above its tag.
403
+ - `scripts/check-frontend-geometry.mjs` (CI) refuses a raw `UModal` (or `LazyUModal`) outside the wrapper, an `AppModal` that names its own width utility (`max-w-*`, `min-w-*`, `w-*`) in `class` or in the `content` slot of `:ui`, or reads either from a variable or a spread it cannot see through, a waiver with no reason, and a waiver with no raw modal directly below it. A `USlideover` is a side panel with no named width, so the guard does not read it.
404
+
392
405
  ### A control is its Nuxt UI component
393
406
 
394
407
  Every CONTROL in the SPA is the Nuxt UI component for it, never the raw HTML element. A raw `<button>` beside a `UButton` is the same control rendered twice, and only one of the two follows the theme, the focus ring, the disabled state and the size scale; the SPA carried 152 of them, 24 raw `<input>`, five hand-built tables and six hand-rolled `<details>`, each with its own border, background and hover recipe.
@@ -1489,7 +1502,10 @@ example ships in [`deploy/frontend`](https://github.com/kibertoad/cat-factory/tr
1489
1502
  `taskExpansion` store and driven by `useTaskExpansion`.
1490
1503
  - **Sidebar & chrome** (`components/layout`): board/account switchers, palettes
1491
1504
  entry points, the language + [interface-mode](#interface-modes-basic--advanced)
1492
- switchers, the `SpendWarningBanner`, and the toolbar (zoom, LOD, decision queue).
1505
+ switchers, the Views switcher (`WorkViewSwitcher`), the `SpendWarningBanner`, and the toolbar
1506
+ (zoom, LOD, and the decision counter, which opens the queue view).
1507
+ - **Queue view** (`components/queue`): `QueueView` and `QueueCard`, the board's second view. It
1508
+ regroups the swimlane verdict and reuses the board card's actions; see [Queue view](#queue-view).
1493
1509
  - **Palettes** (`components/palettes`): drag blocks, pipelines and agents onto
1494
1510
  the board.
1495
1511
  - **Describe new work** (`components/queue/DescribeWorkModal.vue`, `utils/describeWork.ts`): the
@@ -1504,6 +1520,11 @@ example ships in [`deploy/frontend`](https://github.com/kibertoad/cat-factory/tr
1504
1520
  setup. It adds no probe of its own: each row reads what its banner or startup dialog already
1505
1521
  reads (`useSetupChecklist`) and opens the same surface. "Checking" and "missing" are separate
1506
1522
  states, because an unanswered probe and an absent capability are opposite facts.
1523
+ - **Notifications inbox** (`components/layout/NotificationsInbox.vue`, `utils/notificationRoutes.ts`):
1524
+ a card that asks for a person carries its verb ("Answer", "Review", ...) as the primary button,
1525
+ and "Mark read" is secondary. `NOTIFICATION_ROUTES` is exhaustive over the notification types and
1526
+ holds each one's verb and destination, so the title link and the button cannot open different
1527
+ places. A route that can only select the task is named "Open".
1507
1528
  - **Inspector** (`components/panels` + `panels/inspector`): per-block tabs:
1508
1529
  structure, dependencies, model + fragment picker, live execution, and linked
1509
1530
  docs/issues/scenarios. Decisions resolve via `DecisionModal`. A `review` task
@@ -24,6 +24,7 @@ import type { AssistantActionId, AssistantOutcome } from '~/types/domain'
24
24
  import { answerFor, assistantSurface, revealTarget, submitGate } from './AssistantModal.logic'
25
25
  import SectionLabel from '~/components/common/SectionLabel.vue'
26
26
  import Spinner from '~/components/common/Spinner.vue'
27
+ import AppModal from '~/components/common/AppModal.vue'
27
28
 
28
29
  const { t } = useI18n()
29
30
  const ui = useUiStore()
@@ -153,7 +154,7 @@ function reveal(blockId: string): void {
153
154
  </script>
154
155
 
155
156
  <template>
156
- <UModal v-model:open="open" :title="t('assistant.title')" :ui="{ content: 'max-w-2xl' }">
157
+ <AppModal size="form" v-model:open="open" :title="t('assistant.title')">
157
158
  <template #body>
158
159
  <div class="space-y-4">
159
160
  <p class="text-sm text-muted">{{ t('assistant.intro') }}</p>
@@ -361,5 +362,5 @@ function reveal(blockId: string): void {
361
362
  </template>
362
363
  </div>
363
364
  </template>
364
- </UModal>
365
+ </AppModal>
365
366
  </template>
@@ -48,6 +48,7 @@ import { descriptorFieldDefaults } from '@cat-factory/contracts'
48
48
  import { pipelineAllowedForManualStart } from '~/utils/pipeline'
49
49
  import { buildTaskTypePickerRows } from '~/utils/taskTypePicker'
50
50
  import SectionLabel from '~/components/common/SectionLabel.vue'
51
+ import AppModal from '~/components/common/AppModal.vue'
51
52
 
52
53
  const ui = useUiStore()
53
54
  // Interface tier. In BASIC mode this form asks for the task itself (type, title,
@@ -910,7 +911,7 @@ function createRefusalMessage(error: unknown): string | null {
910
911
  </script>
911
912
 
912
913
  <template>
913
- <UModal v-model:open="modalOpen" :title="t('board.addTask.title')">
914
+ <AppModal size="form" v-model:open="modalOpen" :title="t('board.addTask.title')">
914
915
  <template #body>
915
916
  <div class="space-y-4" data-testid="add-task-modal">
916
917
  <p v-if="container" class="text-xs text-muted">
@@ -1459,5 +1460,5 @@ function createRefusalMessage(error: unknown): string | null {
1459
1460
  </UButton>
1460
1461
  </div>
1461
1462
  </template>
1462
- </UModal>
1463
+ </AppModal>
1463
1464
  </template>
@@ -26,6 +26,7 @@ import { GENERIC_PRESET_ID } from '~/stores/initiative'
26
26
  import DescriptorFields from '~/components/common/DescriptorFields.vue'
27
27
  import ContextAttachmentFields from '~/components/context/ContextAttachmentFields.vue'
28
28
  import type { PendingContext } from '~/composables/useContextLinking'
29
+ import AppModal from '~/components/common/AppModal.vue'
29
30
 
30
31
  const ui = useUiStore()
31
32
  const board = useBoardStore()
@@ -175,7 +176,7 @@ async function create() {
175
176
  </script>
176
177
 
177
178
  <template>
178
- <UModal v-model:open="open" :title="t('initiative.create.title')">
179
+ <AppModal size="form" v-model:open="open" :title="t('initiative.create.title')">
179
180
  <template #body>
180
181
  <div class="space-y-4" data-testid="create-initiative-modal">
181
182
  <p v-if="frame" class="text-xs text-muted">
@@ -291,5 +292,5 @@ async function create() {
291
292
  </UButton>
292
293
  </div>
293
294
  </template>
294
- </UModal>
295
+ </AppModal>
295
296
  </template>
@@ -13,6 +13,7 @@ import { apiErrorReason } from '~/composables/api/errors'
13
13
  import { pipelineAllowedForSchedule } from '~/utils/pipeline'
14
14
  import { appliesIntakePredicate } from '~/utils/intakePredicates'
15
15
  import SectionLabel from '~/components/common/SectionLabel.vue'
16
+ import AppModal from '~/components/common/AppModal.vue'
16
17
 
17
18
  const ui = useUiStore()
18
19
  const board = useBoardStore()
@@ -403,7 +404,7 @@ async function add() {
403
404
  </script>
404
405
 
405
406
  <template>
406
- <UModal v-model:open="modalOpen" :title="t('board.recurring.title')">
407
+ <AppModal size="form" v-model:open="modalOpen" :title="t('board.recurring.title')">
407
408
  <template #body>
408
409
  <div class="space-y-4">
409
410
  <p v-if="frame" class="text-xs text-muted">
@@ -689,5 +690,5 @@ async function add() {
689
690
  </UButton>
690
691
  </div>
691
692
  </template>
692
- </UModal>
693
+ </AppModal>
693
694
  </template>
@@ -7,6 +7,7 @@
7
7
  // pages/index.vue. See backend/docs/review-debt-friction.md.
8
8
  import { computed } from 'vue'
9
9
  import SectionLabel from '~/components/common/SectionLabel.vue'
10
+ import AppModal from '~/components/common/AppModal.vue'
10
11
 
11
12
  const { t } = useI18n()
12
13
  const ui = useUiStore()
@@ -76,12 +77,12 @@ function createAnyway() {
76
77
  </script>
77
78
 
78
79
  <template>
79
- <UModal
80
+ <AppModal
81
+ size="prompt"
80
82
  v-model:open="open"
81
83
  :title="title"
82
84
  :dismissible="!pending"
83
85
  :close="{ disabled: pending }"
84
- :ui="{ content: 'max-w-xl' }"
85
86
  >
86
87
  <template #body>
87
88
  <div v-if="ctx" class="space-y-5">
@@ -144,5 +145,5 @@ function createAnyway() {
144
145
  </div>
145
146
  </div>
146
147
  </template>
147
- </UModal>
148
+ </AppModal>
148
149
  </template>
@@ -166,6 +166,8 @@ watch(open, (isOpen) => {
166
166
  </script>
167
167
 
168
168
  <template>
169
+ <!-- workbench-width-ok: a review that puts the evidence files beside every suggestion (768px).
170
+ Its shell waits on #2359. -->
169
171
  <UModal
170
172
  v-model:open="open"
171
173
  :title="t('bootstrap.adoption.title')"
@@ -23,6 +23,7 @@ import VcsConnectSurfaces from '~/components/vcs/VcsConnectSurfaces.vue'
23
23
  import { appInstallationManageUrl, newRepoUrl, VCS_PROVIDER_LABELS } from '~/utils/vcs'
24
24
  import SectionLabel from '~/components/common/SectionLabel.vue'
25
25
  import IconButton from '~/components/common/IconButton.vue'
26
+ import AppModal from '~/components/common/AppModal.vue'
26
27
 
27
28
  const ui = useUiStore()
28
29
  const bootstrap = useBootstrapStore()
@@ -53,7 +54,7 @@ watch(
53
54
  async function loadGitHubRepos() {
54
55
  try {
55
56
  await github.probe()
56
- if (github.connected) await github.load()
57
+ await github.refresh()
57
58
  } catch {
58
59
  // GitHub integration off / unreachable → the repo picker just isn't offered.
59
60
  }
@@ -604,7 +605,7 @@ const statusLabel = computed<Record<BootstrapStatus, string>>(() => ({
604
605
  </script>
605
606
 
606
607
  <template>
607
- <UModal v-model:open="modalOpen" :title="t('bootstrap.title')" :ui="{ content: 'max-w-2xl' }">
608
+ <AppModal size="form" v-model:open="modalOpen" :title="t('bootstrap.title')">
608
609
  <template #body>
609
610
  <div class="space-y-6">
610
611
  <!-- Three states, because each promises the user something different about the repo.
@@ -1080,5 +1081,5 @@ const statusLabel = computed<Record<BootstrapStatus, string>>(() => ({
1080
1081
  </section>
1081
1082
  </div>
1082
1083
  </template>
1083
- </UModal>
1084
+ </AppModal>
1084
1085
  </template>
@@ -0,0 +1,50 @@
1
+ <script setup lang="ts">
2
+ // The app's modal: `UModal` at one of the named overlay widths (`utils/overlaySize.ts`, #2251).
3
+ //
4
+ // `UModal` has no `size` prop, and a `size` variant under `app.config.ts`'s `ui.modal` would never
5
+ // be selected (Nuxt UI passes only `transition`, `fullscreen`, `overlay` and `scrollable` into the
6
+ // variant function), so the width lives here and reaches the modal through `ui.content`. A call
7
+ // site still passes any other `ui` slot, and any other content class (a z-index), unchanged.
8
+ //
9
+ // Every other prop, listener and slot passes straight through to `UModal`. The props interface
10
+ // extends `ModalProps` so a call site is still type-checked against `UModal`, and `@vue-ignore`
11
+ // keeps those props out of the runtime declaration: declared, an absent boolean such as
12
+ // `dismissible` would arrive as `false` and override `UModal`'s own default.
13
+ import type { ModalProps } from '@nuxt/ui'
14
+ import { OVERLAY_WIDTH_CLASS, type OverlaySize } from '~/utils/overlaySize'
15
+
16
+ interface AppModalProps extends /* @vue-ignore */ Omit<ModalProps, 'ui'> {
17
+ /** What kind of dialog this is; the width follows from it. */
18
+ size: OverlaySize
19
+ ui?: ModalProps['ui']
20
+ }
21
+
22
+ const props = defineProps<AppModalProps>()
23
+ defineOptions({ inheritAttrs: false })
24
+
25
+ // A fullscreen modal fills the viewport (`inset-0`, no max-width), so it takes no width. `fullscreen`
26
+ // arrives as an attr, so it is read where the template renders and follows a bound value such as
27
+ // `:fullscreen="isPhone"`. The bare attribute arrives as `''`.
28
+ function isFullscreen(attrs: Record<string, unknown>) {
29
+ return attrs.fullscreen !== undefined && attrs.fullscreen !== false
30
+ }
31
+
32
+ type ContentClass = NonNullable<ModalProps['ui']>['content']
33
+
34
+ // The width joins the caller's `content` value as a class list. A replacer function in that list
35
+ // still gets the plain classes appended after its result (Nuxt UI's `tv.js` finds it inside the
36
+ // array), though the `ui` type only admits a replacer standing alone, hence the cast.
37
+ function uiWithWidth(attrs: Record<string, unknown>): ModalProps['ui'] {
38
+ if (isFullscreen(attrs)) return props.ui
39
+ const content = [OVERLAY_WIDTH_CLASS[props.size], props.ui?.content] as ContentClass
40
+ return { ...props.ui, content }
41
+ }
42
+ </script>
43
+
44
+ <template>
45
+ <UModal v-bind="$attrs" :ui="uiWithWidth($attrs)">
46
+ <template v-for="(_, name) in $slots" #[name]="slotProps">
47
+ <slot :name="name" v-bind="slotProps ?? {}" />
48
+ </template>
49
+ </UModal>
50
+ </template>
@@ -6,6 +6,7 @@
6
6
  // crucially, resolves `false` whenever the modal closes without an explicit choice.
7
7
  import { computed, onUnmounted, watch } from 'vue'
8
8
  import { sharedOverlayStack, type OverlayStackTicket } from '@modular-vue/core'
9
+ import AppModal from '~/components/common/AppModal.vue'
9
10
 
10
11
  const { t } = useI18n()
11
12
  const { open, current, accept, cancel, dismissed } = useConfirm()
@@ -50,10 +51,11 @@ const isDestructive = computed(() => current.value?.variant === 'destructive')
50
51
  the app's full-screen z-50 windows (Model Configuration, Human Test) whose
51
52
  dropdowns sit at z-[60] — the dialog must stack above both or it opens invisibly
52
53
  behind the window. -->
53
- <UModal
54
+ <AppModal
55
+ size="prompt"
54
56
  v-model:open="model"
55
57
  :title="current?.title ?? t('common.confirm.defaultTitle')"
56
- :ui="{ overlay: 'z-[70]', content: 'z-[70] max-w-md' }"
58
+ :ui="{ overlay: 'z-[70]', content: 'z-[70]' }"
57
59
  >
58
60
  <template #body>
59
61
  <div class="flex items-start gap-3" data-testid="confirm-dialog">
@@ -87,5 +89,5 @@ const isDestructive = computed(() => current.value?.variant === 'destructive')
87
89
  </UButton>
88
90
  </div>
89
91
  </template>
90
- </UModal>
92
+ </AppModal>
91
93
  </template>
@@ -1,7 +1,9 @@
1
1
  <script setup lang="ts">
2
- // The keyboard-shortcuts cheatsheet, opened with "?" (or from the command bar). A plain
3
- // UModal listing every global shortcut with its keys rendered as UKbd chips. Driven by the
2
+ // The keyboard-shortcuts cheatsheet, opened with "?" (or from the command bar). A prompt-sized
3
+ // AppModal listing every global shortcut with its keys rendered as UKbd chips. Driven by the
4
4
  // ui store so "?" anywhere toggles it.
5
+ import AppModal from '~/components/common/AppModal.vue'
6
+
5
7
  const { t } = useI18n()
6
8
  const ui = useUiStore()
7
9
 
@@ -29,7 +31,7 @@ const shortcuts = computed(() => [
29
31
  </script>
30
32
 
31
33
  <template>
32
- <UModal v-model:open="open" :title="t('layout.shortcuts.title')" :ui="{ content: 'max-w-md' }">
34
+ <AppModal size="prompt" v-model:open="open" :title="t('layout.shortcuts.title')">
33
35
  <template #body>
34
36
  <dl class="space-y-2" data-testid="shortcuts-help">
35
37
  <div
@@ -44,5 +46,5 @@ const shortcuts = computed(() => [
44
46
  </div>
45
47
  </dl>
46
48
  </template>
47
- </UModal>
49
+ </AppModal>
48
50
  </template>
@@ -4,6 +4,7 @@ import DocumentSyncState from '~/components/documents/DocumentSyncState.vue'
4
4
  import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
5
5
  import SectionLabel from '~/components/common/SectionLabel.vue'
6
6
  import EmptyState from '~/components/common/EmptyState.vue'
7
+ import AppModal from '~/components/common/AppModal.vue'
7
8
 
8
9
  // Import pages from a connected document source and pick one to expand into
9
10
  // board structure. A source selector lets the user choose which connected source
@@ -73,7 +74,7 @@ function preview(externalId: string) {
73
74
  </script>
74
75
 
75
76
  <template>
76
- <UModal v-model:open="open" :title="t('documents.import.title')">
77
+ <AppModal size="form" v-model:open="open" :title="t('documents.import.title')">
77
78
  <template #title>
78
79
  <IntegrationBackTitle :title="t('documents.import.title')" @back="back" />
79
80
  </template>
@@ -163,5 +164,5 @@ function preview(externalId: string) {
163
164
  />
164
165
  </div>
165
166
  </template>
166
- </UModal>
167
+ </AppModal>
167
168
  </template>
@@ -13,6 +13,7 @@
13
13
  // remedy belongs to an admin in a different surface and a dead control states nothing.
14
14
  import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
15
15
  import SecretInput from '~/components/common/SecretInput.vue'
16
+ import AppModal from '~/components/common/AppModal.vue'
16
17
 
17
18
  const { t } = useI18n()
18
19
  const ui = useUiStore()
@@ -109,7 +110,11 @@ async function disconnect() {
109
110
  </script>
110
111
 
111
112
  <template>
112
- <UModal v-model:open="open" :title="descriptor?.label ?? t('documents.connect.title')">
113
+ <AppModal
114
+ size="form"
115
+ v-model:open="open"
116
+ :title="descriptor?.label ?? t('documents.connect.title')"
117
+ >
113
118
  <template #title>
114
119
  <IntegrationBackTitle
115
120
  :title="descriptor?.label ?? t('documents.connect.title')"
@@ -170,5 +175,5 @@ async function disconnect() {
170
175
  </div>
171
176
  </div>
172
177
  </template>
173
- </UModal>
178
+ </AppModal>
174
179
  </template>
@@ -5,6 +5,7 @@ import DocumentOriginLink from '~/components/documents/DocumentOriginLink.vue'
5
5
  import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
6
6
  import SectionLabel from '~/components/common/SectionLabel.vue'
7
7
  import EmptyState from '~/components/common/EmptyState.vue'
8
+ import AppModal from '~/components/common/AppModal.vue'
8
9
 
9
10
  // Manage the workspace's per-DocKind TEMPLATE (singular) + EXEMPLAR (multi) document links (WS1).
10
11
  // A kind can be pointed at one of the workspace's already-imported documents so its parsed
@@ -89,7 +90,7 @@ async function unlink(doc: SourceDocument) {
89
90
  </script>
90
91
 
91
92
  <template>
92
- <UModal v-model:open="open" :title="t('documents.templates.title')">
93
+ <AppModal size="form" v-model:open="open" :title="t('documents.templates.title')">
93
94
  <template #title>
94
95
  <IntegrationBackTitle :title="t('documents.templates.title')" @back="back" />
95
96
  </template>
@@ -238,5 +239,5 @@ async function unlink(doc: SourceDocument) {
238
239
  </template>
239
240
  </div>
240
241
  </template>
241
- </UModal>
242
+ </AppModal>
242
243
  </template>
@@ -2,6 +2,7 @@
2
2
  import { isDesignSource } from '@cat-factory/contracts'
3
3
  import type { DocumentBoardPlan } from '~/types/domain'
4
4
  import Spinner from '~/components/common/Spinner.vue'
5
+ import AppModal from '~/components/common/AppModal.vue'
5
6
 
6
7
  // Preview the structure an imported document expands into, then spawn it. The
7
8
  // plan is fetched fresh on open; a badge makes clear whether an LLM or the
@@ -122,7 +123,7 @@ async function spawn() {
122
123
  </script>
123
124
 
124
125
  <template>
125
- <UModal v-model:open="open" :title="t('documents.spawn.title')">
126
+ <AppModal size="form" v-model:open="open" :title="t('documents.spawn.title')">
126
127
  <template #body>
127
128
  <div class="space-y-4">
128
129
  <div v-if="plan" class="flex items-center justify-between gap-2">
@@ -227,5 +228,5 @@ async function spawn() {
227
228
  </div>
228
229
  </div>
229
230
  </template>
230
- </UModal>
231
+ </AppModal>
231
232
  </template>
@@ -21,6 +21,7 @@
21
21
  import { refRowFor, classifyRefFailure, type RefState } from './ContextDocumentPicker.logic'
22
22
  import type { DocumentSourceKind, ResolvedDocumentRef } from '~/types/domain'
23
23
  import Spinner from '~/components/common/Spinner.vue'
24
+ import AppModal from '~/components/common/AppModal.vue'
24
25
 
25
26
  const { t } = useI18n()
26
27
  const ui = useUiStore()
@@ -151,7 +152,7 @@ function stage() {
151
152
  </script>
152
153
 
153
154
  <template>
154
- <UModal v-model:open="open" :title="t('documents.startFromDesign.title')">
155
+ <AppModal size="form" v-model:open="open" :title="t('documents.startFromDesign.title')">
155
156
  <template #body>
156
157
  <div class="space-y-4">
157
158
  <p class="text-sm text-muted">{{ t('documents.startFromDesign.intro') }}</p>
@@ -232,5 +233,5 @@ function stage() {
232
233
  </div>
233
234
  </div>
234
235
  </template>
235
- </UModal>
236
+ </AppModal>
236
237
  </template>
@@ -35,6 +35,8 @@ const input = computed(() => ({ frameId: ui.environmentWizardFrameId }))
35
35
  </script>
36
36
 
37
37
  <template>
38
+ <!-- workbench-width-ok: a multi-step wizard with a stepper (768px). Its shell waits on #2359.
39
+ -->
38
40
  <UModal
39
41
  v-model:open="open"
40
42
  :title="t('environmentWizard.title')"
@@ -19,6 +19,8 @@ const open = computed({
19
19
  </script>
20
20
 
21
21
  <template>
22
+ <!-- workbench-width-ok: a library of the merged service catalog (768px). Its shell waits on
23
+ #2359. -->
22
24
  <UModal
23
25
  v-model:open="open"
24
26
  :title="t('foundational.panel.title')"
@@ -18,6 +18,8 @@ const open = computed({
18
18
  </script>
19
19
 
20
20
  <template>
21
+ <!-- workbench-width-ok: a library of the merged fragment catalog (768px). Its shell waits on
22
+ #2359. -->
21
23
  <UModal v-model:open="open" :title="t('fragments.panel.title')" :ui="{ content: 'max-w-3xl' }">
22
24
  <template #body>
23
25
  <!-- The anchor for the tutorial tour about steering agents with standards, which points at
@@ -29,6 +29,7 @@ import ServiceFragments from '~/components/panels/inspector/ServiceFragments.vue
29
29
  import { appInstallationManageUrl, VCS_PROVIDER_LABELS } from '~/utils/vcs'
30
30
  import SectionLabel from '~/components/common/SectionLabel.vue'
31
31
  import IconButton from '~/components/common/IconButton.vue'
32
+ import AppModal from '~/components/common/AppModal.vue'
32
33
 
33
34
  const { t } = useI18n()
34
35
 
@@ -58,10 +59,10 @@ const adding = ref(false)
58
59
  async function loadRepos() {
59
60
  try {
60
61
  await github.probe()
61
- // Load only the linked-repo projection (to flag repos already on the board). The
62
+ // Load the linked-repo projection once per board (to flag repos already on the board). The
62
63
  // available-repos list is NOT prefetched — it's searched server-side on demand once
63
64
  // the user types (see the repoQueryRaw watcher), so a wide install isn't shipped whole.
64
- if (github.connected) await github.load()
65
+ await github.refresh()
65
66
  } catch {
66
67
  // Integration off / unreachable → the picker stays empty, GitHubConnect shows.
67
68
  }
@@ -437,7 +438,7 @@ function done() {
437
438
  </script>
438
439
 
439
440
  <template>
440
- <UModal v-model:open="open" :title="t('github.addService.title')" :ui="{ content: 'max-w-xl' }">
441
+ <AppModal size="form" v-model:open="open" :title="t('github.addService.title')">
441
442
  <template #body>
442
443
  <div class="space-y-6">
443
444
  <p class="text-sm text-muted">
@@ -682,5 +683,5 @@ function done() {
682
683
  </template>
683
684
  </div>
684
685
  </template>
685
- </UModal>
686
+ </AppModal>
686
687
  </template>
@@ -13,6 +13,7 @@ import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
13
13
  import { VCS_PROVIDER_ICONS, VCS_PROVIDER_LABELS } from '~/utils/vcs'
14
14
  import SectionLabel from '~/components/common/SectionLabel.vue'
15
15
  import EmptyState from '~/components/common/EmptyState.vue'
16
+ import AppModal from '~/components/common/AppModal.vue'
16
17
 
17
18
  const { t } = useI18n()
18
19
  const ui = useUiStore()
@@ -99,7 +100,7 @@ watch(
99
100
  open,
100
101
  (isOpen) => {
101
102
  if (!isOpen) return
102
- if (github.connected) void github.load()
103
+ if (github.connected) void github.refresh()
103
104
  },
104
105
  { immediate: true },
105
106
  )
@@ -287,7 +288,7 @@ async function merge(pr: GitHubPullRequest) {
287
288
  </script>
288
289
 
289
290
  <template>
290
- <UModal v-model:open="open" :title="panelTitle" :ui="{ content: 'max-w-2xl' }">
291
+ <AppModal size="form" v-model:open="open" :title="panelTitle">
291
292
  <template #title>
292
293
  <IntegrationBackTitle :title="panelTitle" @back="back" />
293
294
  </template>
@@ -716,5 +717,5 @@ async function merge(pr: GitHubPullRequest) {
716
717
  </template>
717
718
  </div>
718
719
  </template>
719
- </UModal>
720
+ </AppModal>
720
721
  </template>
@@ -2,6 +2,7 @@
2
2
  import type { DropdownMenuItem } from '@nuxt/ui'
3
3
  import type { CloudProvider } from '~/types/domain'
4
4
  import SectionLabel from '~/components/common/SectionLabel.vue'
5
+ import AppModal from '~/components/common/AppModal.vue'
5
6
 
6
7
  // Account + board switching. Picks the active account (personal / org) and the
7
8
  // active board within it, and manages boards (new / rename / delete). The account
@@ -322,7 +323,11 @@ async function submitPrompt() {
322
323
  </UDropdownMenu>
323
324
 
324
325
  <!-- create / rename prompt -->
325
- <UModal v-model:open="promptOpen" :title="prompt ? promptMeta[prompt].title : ''">
326
+ <AppModal
327
+ size="prompt"
328
+ v-model:open="promptOpen"
329
+ :title="prompt ? promptMeta[prompt].title : ''"
330
+ >
326
331
  <template #body>
327
332
  <UForm class="space-y-3" @submit="submitPrompt">
328
333
  <UFormField :label="t('layout.boardSwitcher.prompt.nameLabel')">
@@ -364,6 +369,6 @@ async function submitPrompt() {
364
369
  </div>
365
370
  </UForm>
366
371
  </template>
367
- </UModal>
372
+ </AppModal>
368
373
  </div>
369
374
  </template>
@@ -8,6 +8,7 @@
8
8
 
9
9
  import SectionLabel from '~/components/common/SectionLabel.vue'
10
10
  import EmptyState from '~/components/common/EmptyState.vue'
11
+ import AppModal from '~/components/common/AppModal.vue'
11
12
 
12
13
  interface Command {
13
14
  id: string
@@ -256,7 +257,7 @@ function indexOf(cmd: Command) {
256
257
  </script>
257
258
 
258
259
  <template>
259
- <UModal v-model:open="open" :ui="{ content: 'max-w-xl' }">
260
+ <AppModal size="form" v-model:open="open">
260
261
  <template #content>
261
262
  <div class="flex flex-col" data-testid="command-bar" @keydown="onKeydown">
262
263
  <div class="flex items-center gap-2 border-b border-default px-3">
@@ -307,5 +308,5 @@ function indexOf(cmd: Command) {
307
308
  </div>
308
309
  </div>
309
310
  </template>
310
- </UModal>
311
+ </AppModal>
311
312
  </template>