@1agh/maude 0.43.0 → 0.44.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.
Files changed (122) hide show
  1. package/apps/studio/acp/bridge.ts +44 -10
  2. package/apps/studio/acp/env.ts +23 -0
  3. package/apps/studio/acp/login-state.ts +263 -0
  4. package/apps/studio/acp/plugin-bootstrap.ts +31 -25
  5. package/apps/studio/acp/probe.ts +152 -1
  6. package/apps/studio/annotations-layer.tsx +396 -46
  7. package/apps/studio/api.ts +395 -2
  8. package/apps/studio/bin/_audio-search.mjs +140 -0
  9. package/apps/studio/bin/_import-asset-pdf-worker.mjs +34 -0
  10. package/apps/studio/bin/_import-asset.mjs +815 -0
  11. package/apps/studio/bin/_import-brand.mjs +635 -0
  12. package/apps/studio/bin/_import-tokens-alias-resolver.mjs +95 -0
  13. package/apps/studio/bin/_import-tokens.mjs +1201 -0
  14. package/apps/studio/bin/_transcribe.mjs +419 -0
  15. package/apps/studio/bin/_transcribe.test.mjs +80 -0
  16. package/apps/studio/bin/audio-search.sh +28 -0
  17. package/apps/studio/bin/generate.sh +154 -0
  18. package/apps/studio/bin/import-asset.sh +34 -0
  19. package/apps/studio/bin/import-brand.sh +29 -0
  20. package/apps/studio/bin/import-tokens.sh +33 -0
  21. package/apps/studio/bin/photo-bg-remove.sh +119 -21
  22. package/apps/studio/bin/transcribe.sh +39 -0
  23. package/apps/studio/canvas-edit.ts +197 -2
  24. package/apps/studio/canvas-lib.tsx +115 -6
  25. package/apps/studio/client/app.jsx +1954 -685
  26. package/apps/studio/client/generate-dialog.jsx +352 -0
  27. package/apps/studio/client/github.js +14 -0
  28. package/apps/studio/client/inspector-controls.jsx +781 -0
  29. package/apps/studio/client/panels/BrandUploadPanel.jsx +233 -0
  30. package/apps/studio/client/panels/ChatPanel.jsx +15 -33
  31. package/apps/studio/client/panels/IntroVideoDialog.jsx +44 -0
  32. package/apps/studio/client/panels/OnboardingWizard.jsx +12 -0
  33. package/apps/studio/client/panels/ReadinessList.jsx +227 -7
  34. package/apps/studio/client/panels/SettingsPanel.jsx +828 -0
  35. package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
  36. package/apps/studio/client/photo-knobs.jsx +167 -172
  37. package/apps/studio/client/styles/3-shell-maude.css +334 -28
  38. package/apps/studio/client/styles/4-components.css +223 -0
  39. package/apps/studio/client/styles/6-acp-chat.css +86 -0
  40. package/apps/studio/client/tour/quick-setup-tour.js +39 -0
  41. package/apps/studio/commands/annotation-strokes-command.ts +13 -3
  42. package/apps/studio/config.schema.json +70 -0
  43. package/apps/studio/context-menu.tsx +11 -2
  44. package/apps/studio/context.ts +16 -0
  45. package/apps/studio/design-setup-readiness.ts +115 -0
  46. package/apps/studio/dist/client.bundle.js +2097 -2088
  47. package/apps/studio/dist/styles.css +1 -1
  48. package/apps/studio/draw/palette.ts +34 -0
  49. package/apps/studio/footage/schema.test.ts +92 -0
  50. package/apps/studio/footage/schema.ts +179 -1
  51. package/apps/studio/generation/adapters/elevenlabs.ts +370 -0
  52. package/apps/studio/generation/adapters/gemini.ts +542 -0
  53. package/apps/studio/generation/adapters/groq.test.ts +80 -0
  54. package/apps/studio/generation/adapters/groq.ts +247 -0
  55. package/apps/studio/generation/audio-library.test.ts +95 -0
  56. package/apps/studio/generation/audio-library.ts +156 -0
  57. package/apps/studio/generation/captions.test.ts +78 -0
  58. package/apps/studio/generation/captions.ts +132 -0
  59. package/apps/studio/generation/download.ts +76 -0
  60. package/apps/studio/generation/elevenlabs.test.ts +223 -0
  61. package/apps/studio/generation/gemini.test.ts +350 -0
  62. package/apps/studio/generation/jobs.test.ts +128 -0
  63. package/apps/studio/generation/jobs.ts +243 -0
  64. package/apps/studio/generation/keys.test.ts +75 -0
  65. package/apps/studio/generation/keys.ts +162 -0
  66. package/apps/studio/generation/prefs.test.ts +95 -0
  67. package/apps/studio/generation/prefs.ts +82 -0
  68. package/apps/studio/generation/registry.ts +88 -0
  69. package/apps/studio/generation/types.test.ts +123 -0
  70. package/apps/studio/generation/types.ts +266 -0
  71. package/apps/studio/generation/whisper-models.test.ts +152 -0
  72. package/apps/studio/generation/whisper-models.ts +257 -0
  73. package/apps/studio/generation/whisper-spike-results.md +61 -0
  74. package/apps/studio/git/service.ts +3 -1
  75. package/apps/studio/http.ts +867 -9
  76. package/apps/studio/media/intro.mp4 +0 -0
  77. package/apps/studio/media-commit-chain.ts +119 -0
  78. package/apps/studio/paths.ts +10 -0
  79. package/apps/studio/readiness.ts +108 -20
  80. package/apps/studio/scaffold-design.ts +1145 -0
  81. package/apps/studio/server.ts +13 -1
  82. package/apps/studio/test/_helpers.ts +15 -2
  83. package/apps/studio/test/acp-bridge.test.ts +34 -1
  84. package/apps/studio/test/acp-env.test.ts +30 -0
  85. package/apps/studio/test/acp-plugin-bootstrap.test.ts +23 -57
  86. package/apps/studio/test/acp-session-plugins.test.ts +72 -1
  87. package/apps/studio/test/annotation-strokes-command.test.ts +17 -0
  88. package/apps/studio/test/annotations-layer.test.ts +174 -0
  89. package/apps/studio/test/canvas-origin-gate.test.ts +37 -0
  90. package/apps/studio/test/csp-canvas-shell.test.ts +11 -0
  91. package/apps/studio/test/csrf-write-guard.test.ts +23 -1
  92. package/apps/studio/test/design-setup-readiness.test.ts +155 -0
  93. package/apps/studio/test/element-structural-edit.test.ts +108 -0
  94. package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
  95. package/apps/studio/test/generate-route.test.ts +106 -0
  96. package/apps/studio/test/import-asset-browser.test.ts +64 -0
  97. package/apps/studio/test/import-asset-route.test.ts +71 -0
  98. package/apps/studio/test/import-asset.test.ts +399 -0
  99. package/apps/studio/test/import-brand-browser.test.ts +88 -0
  100. package/apps/studio/test/import-brand-route.test.ts +120 -0
  101. package/apps/studio/test/import-brand.test.ts +206 -0
  102. package/apps/studio/test/import-tokens.test.ts +722 -0
  103. package/apps/studio/test/media-commit-chain.test.ts +210 -0
  104. package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -0
  105. package/apps/studio/test/readiness.test.ts +13 -11
  106. package/apps/studio/test/scaffold-design.test.ts +122 -0
  107. package/apps/studio/test/tour-overlay.test.tsx +16 -0
  108. package/apps/studio/tool-palette.tsx +11 -6
  109. package/apps/studio/ui-prefs.ts +130 -0
  110. package/apps/studio/use-canvas-media-drop.tsx +4 -2
  111. package/apps/studio/use-chrome-visibility.tsx +8 -2
  112. package/apps/studio/whats-new.json +96 -0
  113. package/apps/studio/ws.ts +5 -0
  114. package/cli/bin/maude.mjs +6 -6
  115. package/cli/commands/design.mjs +33 -0
  116. package/cli/lib/gitignore-block.mjs +1 -0
  117. package/cli/lib/pkg-root.mjs +107 -0
  118. package/cli/lib/pkg-root.test.mjs +79 -0
  119. package/cli/lib/reconstruct-toolset.test.mjs +194 -0
  120. package/cli/lib/update-check.mjs +8 -0
  121. package/package.json +12 -10
  122. package/plugins/design/dependencies.json +18 -0
Binary file
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @file media-commit-chain.ts — serialized accumulator chain for
3
+ * concurrent async media-intake commits.
4
+ * @scope apps/studio/media-commit-chain.ts
5
+ * @purpose Fixes the Phase-23 batch-drop data-loss bug: a Finder drop of
6
+ * N files fires N independent async completions (image decode /
7
+ * asset upload), each of which used to read a `strokesRef.current`
8
+ * mirror and call `commitStrokes(before, after)` on its own.
9
+ * `strokesRef` only catches up on the NEXT render, so two
10
+ * completions landing before a render commit both compute
11
+ * `before` from the same stale snapshot — the second commit's
12
+ * `setStrokesState` overwrites the first's, silently dropping it.
13
+ *
14
+ * This chain fixes it by threading the PREVIOUS link's resulting
15
+ * array forward as the next link's `before` while links are
16
+ * in flight (Promise-chained, mirroring `editApplyChainRef` /
17
+ * `structuralWrite` in client/app.jsx). The subtle part is what
18
+ * happens once the queue drains: a naive "go idle, trust a fresh
19
+ * `getSnapshot()` again" reset raced in practice — `getSnapshot()`
20
+ * (`strokesRef.current`) only catches up on React's NEXT render,
21
+ * which can take longer than the handful of microtask ticks this
22
+ * chain itself needs to settle a link, so a fast-idling chain
23
+ * could hand the next link a `getSnapshot()` that hasn't caught
24
+ * up yet — reproducing the same loss one level down (confirmed
25
+ * empirically against a real dev server: see the Task 5 write-up).
26
+ * Instead, every idle read is RECONCILED against `remembered`
27
+ * (the last array this chain itself produced): for every id the
28
+ * chain itself has ever touched, `remembered`'s value WINS
29
+ * outright, even if the fresh snapshot also contains that id —
30
+ * presence alone isn't enough, since fresh can hold a STALE
31
+ * value for an id the chain already updated (see `reconcile()`
32
+ * below and DDR-165). Only ids fresh has that the chain has
33
+ * never touched get folded in from it, which is what still lets
34
+ * an unrelated synchronous edit (pen/shape/drag, made while
35
+ * idle) through.
36
+ *
37
+ * Framework/DOM-free by design so the no-loss guarantee is
38
+ * testable without React or a real image decode/upload — see
39
+ * test/media-commit-chain.test.ts.
40
+ */
41
+
42
+ export interface MediaCommitResult<T> {
43
+ /** The chain's new accumulator value — what the NEXT queued link builds on. */
44
+ after: readonly T[];
45
+ /**
46
+ * Override the `before` passed to `onCommit` (defaults to this link's
47
+ * accumulator `before`). Lets a caller exclude an ephemeral, never-
48
+ * separately-committed entry (e.g. an optimistic blob: preview) from the
49
+ * undo record while still folding it into the accumulator correctly.
50
+ */
51
+ commitBefore?: readonly T[];
52
+ label?: string;
53
+ }
54
+
55
+ export interface MediaCommitChain<T> {
56
+ /**
57
+ * Enqueue a synchronous mutation. `mutate` receives the current
58
+ * accumulated `before` (the previous link's `after` while a link is still
59
+ * in flight, or a reconciled `getSnapshot()` read once the queue has
60
+ * drained) and returns a result, or `null` to skip (no-op, `before` passes
61
+ * through unchanged). `onCommit` fires with `(before, after, label)` in
62
+ * strict queue order, once per non-null mutation.
63
+ */
64
+ enqueue(
65
+ mutate: (before: readonly T[]) => MediaCommitResult<T> | null,
66
+ onCommit: (before: readonly T[], after: readonly T[], label?: string) => void
67
+ ): Promise<readonly T[]>;
68
+ }
69
+
70
+ export function createMediaCommitChain<T>(
71
+ getSnapshot: () => readonly T[],
72
+ keyOf: (item: T) => string
73
+ ): MediaCommitChain<T> {
74
+ let pending: Promise<readonly T[]> | null = null;
75
+ // The last array this chain itself produced. Never cleared — only ever
76
+ // superseded by a fresh reconciled read once every id it carries shows up
77
+ // in `getSnapshot()` too (see reconcile()).
78
+ let remembered: readonly T[] | null = null;
79
+
80
+ function reconcile(): readonly T[] {
81
+ const fresh = getSnapshot();
82
+ if (!remembered) return fresh;
83
+ // Prefer `remembered`'s value for any id the CHAIN itself produced —
84
+ // not just fold in ids fresh is missing. `fresh` (getSnapshot) can
85
+ // contain a STALE value for an id the chain already swapped: e.g. an
86
+ // image stroke's optimistic (blob:) href is set via a separate, non-
87
+ // chain setState, and `fresh` catches up to THAT before it catches up
88
+ // to the chain's OWN later commit swapping it to the real href — same
89
+ // id, stale value. Checking "is the id present" (the previous version
90
+ // of this function) said "yes, already rendered" and used fresh's
91
+ // stale copy, silently reverting the swap — which then gets filtered
92
+ // out entirely as still-ephemeral at the persistence layer. `remembered`
93
+ // is always at least as fresh as anything the chain has itself
94
+ // touched, so it wins on overlap; anything fresh has that remembered
95
+ // doesn't (an external, non-chain edit made while idle) still comes
96
+ // through via the second spread.
97
+ const rememberedKeys = new Set(remembered.map(keyOf));
98
+ const notInRemembered = fresh.filter((item) => !rememberedKeys.has(keyOf(item)));
99
+ return [...remembered, ...notInRemembered];
100
+ }
101
+
102
+ const enqueue: MediaCommitChain<T>['enqueue'] = (mutate, onCommit) => {
103
+ const seed = pending ?? Promise.resolve(reconcile());
104
+ const link: Promise<readonly T[]> = seed.then((before) => {
105
+ const result = mutate(before);
106
+ if (!result) return before;
107
+ onCommit(result.commitBefore ?? before, result.after, result.label);
108
+ remembered = result.after;
109
+ return result.after;
110
+ });
111
+ pending = link;
112
+ void link.then(() => {
113
+ if (pending === link) pending = null;
114
+ });
115
+ return link;
116
+ };
117
+
118
+ return { enqueue };
119
+ }
@@ -58,6 +58,16 @@ export const RUNTIME_BUNDLES_DIR: string = join(DIST_DIR, 'runtime');
58
58
  */
59
59
  export const STICKERS_DIR: string = join(DEV_SERVER_ROOT, 'stickers');
60
60
 
61
+ /**
62
+ * `<DEV_SERVER_ROOT>/media/` — bundled Maude-product media (the intro
63
+ * showreel, DDR-166 Phase 1 T2). Same shape as `STICKERS_DIR`: ships with the
64
+ * `@1agh/maude` tarball (a subdir of `apps/studio`, already covered by the
65
+ * existing `files` entry), resolved from DEV_SERVER_ROOT per DDR-045 — this is
66
+ * MAUDE's OWN asset store, never the served project's `.design/assets/`, so
67
+ * every user sees the same intro regardless of which project is open.
68
+ */
69
+ export const MEDIA_DIR: string = join(DEV_SERVER_ROOT, 'media');
70
+
61
71
  /**
62
72
  * Absolute path to a bundled plugin's loadable tree (`commands/`, `agents/`,
63
73
  * `skills/`, `hooks/`, `.claude-plugin/plugin.json`), or `null` when this layout
@@ -14,6 +14,7 @@
14
14
  import { existsSync, readFileSync } from 'node:fs';
15
15
  import { homedir } from 'node:os';
16
16
  import { join } from 'node:path';
17
+ import { getClaudeAuthStatus } from './acp/login-state.ts';
17
18
  import { isNativePluginContext } from './acp/plugin-bootstrap.ts';
18
19
  import { resolveAdapterEntry, resolveClaudePath } from './acp/probe.ts';
19
20
  import { resolveBrowser } from './bin/_ensure-browser.mjs';
@@ -32,6 +33,19 @@ export interface ReadinessItem {
32
33
  detail: string;
33
34
  /** Copy-paste-able fix, present only when the item is not satisfied. */
34
35
  remediation?: string;
36
+ /** Verbatim shell command for a dedicated code-block + Copy affordance (DDR-166 T0c) — kept separate from the prose `remediation`. */
37
+ installCommand?: string;
38
+ /** UI action hint (DDR-166): 'install' renders a "Set up AI editing" button that runs the official installer then auto-chains to sign-in; 'signin' renders a "Sign in to Claude" button alone. */
39
+ action?: 'install' | 'signin';
40
+ /**
41
+ * DDR-166 Decision 3 (binding — a security-review-mandated control, not
42
+ * cosmetic): the resolved absolute path of the binary a Sign-in click is
43
+ * about to spawn, shown BEFORE the button so a pre-existing PATH-hijacked
44
+ * `claude` isn't invisible to the one human capable of noticing it's wrong.
45
+ */
46
+ resolvedPath?: string;
47
+ /** True when `resolvedPath` is the binary Maude itself just installed (content-pinned — see probe.ts `setTrustedClaudeBin`), not something already on the user's PATH. */
48
+ resolvedViaMaude?: boolean;
35
49
  }
36
50
 
37
51
  export interface ReadinessReport {
@@ -164,37 +178,101 @@ export function scanPlugins(): PluginScan {
164
178
  * (DDR-128 hardening) so this can't be turned into a spawn-storm from a drive-by page.
165
179
  */
166
180
  export async function probeReadiness(): Promise<ReadinessReport> {
167
- const [claude, maude, agentBrowser] = await Promise.all([
181
+ let [claude, maude, agentBrowser] = await Promise.all([
168
182
  (async () => resolveClaudePath() ?? (await resolveOnPath('claude')))(),
169
183
  resolveOnPath('maude'),
170
184
  resolveOnPath('agent-browser'),
171
185
  ]);
172
186
 
187
+ // Deterministic E2E stub for the two claude-readiness states that are
188
+ // otherwise near-impossible to reproduce on a real, already-set-up dev
189
+ // machine (mirrors MAUDE_E2E_FAKE_GITHUB_LOGIN in oauth.rs — same problem,
190
+ // same shape of fix: a real state that needs a pristine/logged-out machine
191
+ // to occur naturally gets a narrow, explicitly-named override instead of a
192
+ // hand-rolled sandbox that fights the OS's own PATH resolution). Never
193
+ // affects real users — unset in every normal launch.
194
+ const e2eForce = process.env.MAUDE_E2E_FORCE_CLAUDE_STATUS;
195
+ // A verified, explicitly-provisioned MAUDE_CLAUDE_BIN override (T0c's
196
+ // install flow, once its freshness+hash check passes) must never be
197
+ // clobbered back to "missing"/"signed-out" by this coarser stub — mirrors
198
+ // the same precedence fix in probe.ts's resolveClaudePath(). Without this,
199
+ // the acp-cold-start scenario could never observe its OWN install actually
200
+ // taking effect after starting from a forced-missing state.
201
+ const hasVerifiedOverride =
202
+ !!process.env.MAUDE_CLAUDE_BIN && claude === process.env.MAUDE_CLAUDE_BIN;
203
+ if (e2eForce === 'missing' && !hasVerifiedOverride) claude = null;
204
+ else if (e2eForce === 'signed-out' && !hasVerifiedOverride) claude = claude || '/usr/bin/claude'; // present, but auth probe below is also stubbed
205
+
173
206
  const items: ReadinessItem[] = [];
174
207
 
208
+ // DDR-166 T0d — honest 3-state instead of presence-only: not installed (an
209
+ // "install" action, T0c — runs the same verbatim official one-liner a user
210
+ // would type themselves, live-verified 2026-07-13 against
211
+ // code.claude.com/docs/en/quickstart.md; the copy-paste command below stays
212
+ // as the fallback/opt-out path) / installed-but-signed-out (a "Sign in"
213
+ // action, T0d) / signed-in-on-subscription.
214
+ const authStatus =
215
+ e2eForce === 'signed-out' ? { loggedIn: false } : claude ? await getClaudeAuthStatus() : null;
216
+ const signedIn = !!authStatus?.loggedIn;
217
+ const offSubscription =
218
+ signedIn && authStatus?.apiProvider && authStatus.apiProvider !== 'firstParty';
219
+ // DDR-166 Decision 5 — the settings-UI opt-out (prefs.rs `claude_auto_setup`,
220
+ // DEFAULT ON) reaches the dev-server as an env var set fresh at each sidecar
221
+ // spawn. Disabled → fall back to guide-only: no automated action offered,
222
+ // same posture as before T0c/T0d existed (still shows the copy-paste
223
+ // command below, just no button).
224
+ const autoSetupEnabled = process.env.MAUDE_CLAUDE_AUTOSETUP_ENABLED !== '0';
175
225
  items.push({
176
226
  id: 'claude',
177
227
  label: 'Claude Code (the `claude` CLI)',
178
228
  required: true,
179
- status: claude ? 'present' : 'missing',
180
- detail: claude
181
- ? 'Installed AI editing drives it on your own Pro/Max subscription.'
182
- : 'Not found on PATH.',
183
- remediation: claude
184
- ? undefined
185
- : 'Install Claude Code, then run `claude` and `/login` once. AI editing runs on your own Pro/Max subscription — never API billing.',
229
+ status: claude && signedIn ? 'present' : 'missing',
230
+ detail: !claude
231
+ ? 'Not found on PATH.'
232
+ : signedIn
233
+ ? offSubscription
234
+ ? `Signed in, but not on a Claude subscription (${authStatus?.apiProvider}) — AI editing may bill metered API usage.`
235
+ : `Signed in${authStatus?.subscriptionType ? ` · ${authStatus.subscriptionType}` : ''} AI editing runs on your own Pro/Max subscription.`
236
+ : 'Installed, but not signed in.',
237
+ remediation: !claude
238
+ ? 'Install Claude Code, then sign in. AI editing runs on your own Pro/Max subscription — never API billing.'
239
+ : signedIn
240
+ ? undefined
241
+ : 'Sign in to connect it to your Pro/Max subscription.',
242
+ installCommand: !claude ? 'curl -fsSL https://claude.ai/install.sh | bash' : undefined,
243
+ action: !autoSetupEnabled ? undefined : !claude ? 'install' : !signedIn ? 'signin' : undefined,
244
+ resolvedPath: claude ?? undefined,
245
+ resolvedViaMaude: !!claude && claude === process.env.MAUDE_CLAUDE_BIN,
186
246
  });
187
247
 
188
- items.push({
189
- id: 'maude',
190
- label: 'maude CLI',
191
- required: true,
192
- status: maude ? 'present' : 'missing',
193
- detail: maude ? 'On PATH — `/design:edit` can reach its helpers.' : 'Not found on PATH.',
194
- remediation: maude
195
- ? undefined
196
- : 'Install it: `npm i -g @1agh/maude`. `/design:edit` shells out to `maude design …`, so it must be on PATH.',
197
- });
248
+ // DDR-166 T0b / DDR-168 — `maude` is bundled into the packaged app (compiled
249
+ // via apps/desktop/scripts/build-cli-binary.mjs, shipped as `externalBin`
250
+ // `binaries/maude`; sidecar.rs stages it into its own narrow, single-binary
251
+ // PATH directory, PREPENDS that directory ahead of the rest of PATH, and sets
252
+ // MAUDE_BUNDLED_CLI_PATH to that exact path). Prepending makes this
253
+ // deterministic, not a best-effort fallback: inside the sidecar's own spawned
254
+ // children the bundled copy always wins PATH resolution, so `maude === bundledCliPath`
255
+ // is the normal case on the desktop path, not merely one possible outcome. This
256
+ // row disappears entirely for the end user rather than surfacing an
257
+ // `npm i -g` instruction for something already shipped — it's an internal
258
+ // plumbing detail, not a recognizable capability like the agent-browser/
259
+ // adapter rows below. In dev contexts where the bundling step hasn't run
260
+ // (a bare `bun run server.ts`, `maude design serve`), MAUDE_BUNDLED_CLI_PATH
261
+ // is unset and this falls through to the pre-T0b behavior unchanged.
262
+ const bundledCliPath = process.env.MAUDE_BUNDLED_CLI_PATH || null;
263
+ const maudeIsBundled = !!bundledCliPath && maude === bundledCliPath;
264
+ if (!maudeIsBundled) {
265
+ items.push({
266
+ id: 'maude',
267
+ label: 'maude CLI',
268
+ required: true,
269
+ status: maude ? 'present' : 'missing',
270
+ detail: maude ? 'On PATH — `/design:edit` can reach its helpers.' : 'Not found on PATH.',
271
+ remediation: maude
272
+ ? undefined
273
+ : 'Install it: `npm i -g @1agh/maude`. `/design:edit` shells out to `maude design …`, so it must be on PATH.',
274
+ });
275
+ }
198
276
 
199
277
  const scan = scanPlugins();
200
278
  // DDR-143 — on the native/desktop path the ACP chat session AUTO-LOADS the
@@ -206,8 +284,18 @@ export async function probeReadiness(): Promise<ReadinessReport> {
206
284
  // `design` alone — demanding a plugin the chat doesn't use would be a false red.
207
285
  // The web `maude design serve` path ships no bundle (native=false, dir null) →
208
286
  // the manual marketplace remediation still shows there.
287
+ //
288
+ // DDR-168 — `designAutoloaded` no longer requires `!scan.design`: the bundled
289
+ // copy is injected unconditionally now (plugin-bootstrap.ts), regardless of
290
+ // whether a marketplace copy is ALSO installed at the user level (the
291
+ // double-registration risk that used to gate this is closed structurally in
292
+ // bridge.ts's `options.settings.enabledPlugins` override instead). So this row
293
+ // reads "auto-loaded" even on a machine that separately has design@maude
294
+ // installed — that's the whole point: the bundled, release-matched copy is
295
+ // what actually runs in the chat, so the UI should say so rather than imply
296
+ // "ready because you already had it."
209
297
  const native = isNativePluginContext();
210
- const designAutoloaded = native && DESIGN_PLUGIN_DIR !== null && !scan.design;
298
+ const designAutoloaded = native && DESIGN_PLUGIN_DIR !== null;
211
299
  const designReady = scan.design || designAutoloaded;
212
300
  const pluginStatus: ReadinessStatus = designReady
213
301
  ? 'present'
@@ -221,7 +309,7 @@ export async function probeReadiness(): Promise<ReadinessReport> {
221
309
  status: pluginStatus,
222
310
  detail: designReady
223
311
  ? designAutoloaded
224
- ? 'Auto-loaded in the Maude chat session nothing to install.'
312
+ ? 'Bundled with this app loads automatically in the chat session.'
225
313
  : 'design@maude is installed.'
226
314
  : scan.status === 'unknown'
227
315
  ? "Couldn't read Claude Code's plugin registry — check it manually."