@1agh/maude 0.31.0 → 0.32.1
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.
- package/apps/studio/acp/bridge.ts +18 -1
- package/apps/studio/client/app.jsx +7 -0
- package/apps/studio/client/panels/ChatPanel.jsx +46 -20
- package/apps/studio/client/panels/OnboardingWizard.jsx +35 -0
- package/apps/studio/client/panels/ReadinessList.jsx +189 -0
- package/apps/studio/client/styles/3-shell-maude.css +12 -0
- package/apps/studio/client/styles/6-acp-chat.css +50 -0
- package/apps/studio/dist/client.bundle.js +2838 -2521
- package/apps/studio/dist/styles.css +186 -0
- package/apps/studio/http.ts +15 -0
- package/apps/studio/readiness.ts +238 -0
- package/apps/studio/test/acp-bridge.test.ts +4 -0
- package/apps/studio/test/acp-origin-gate.test.ts +17 -0
- package/apps/studio/test/fixtures/mock-acp-agent.mjs +2 -1
- package/apps/studio/test/readiness.test.ts +154 -0
- package/apps/studio/whats-new.json +18 -0
- package/package.json +8 -8
|
@@ -132,13 +132,30 @@ export class AcpBridge {
|
|
|
132
132
|
if (!adapterEntry) {
|
|
133
133
|
throw new Error('The Claude agent bridge is not installed in this build.');
|
|
134
134
|
}
|
|
135
|
-
|
|
135
|
+
const claudePath = resolveClaudePath();
|
|
136
|
+
if (!claudePath) {
|
|
136
137
|
throw new Error("Claude Code isn't connected — run `claude` in a terminal and `/login`.");
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
// DDR-123 guardrail #1 — strip ANTHROPIC_API_KEY so the child stays on the
|
|
140
141
|
// user's subscription. This is the whole compliance story; do not weaken it.
|
|
141
142
|
const env = scrubAgentEnv(process.env);
|
|
143
|
+
// DDR-123 guardrail #2 — pin the adapter to the user's OWN `claude` CLI.
|
|
144
|
+
// `claude-agent-acp`'s `claudeCliPath()` honors CLAUDE_CODE_EXECUTABLE and
|
|
145
|
+
// ONLY otherwise falls back to the ~210 MB native Claude binary shipped as a
|
|
146
|
+
// platform-specific OPTIONAL dep of @anthropic-ai/claude-agent-sdk. The
|
|
147
|
+
// desktop bundle deliberately stages just the adapter's JS closure (not that
|
|
148
|
+
// native binary — see apps/desktop/scripts/stage-resources.mjs), so without
|
|
149
|
+
// this pin the packaged adapter would throw "native binary not found". Driving
|
|
150
|
+
// the user's installed CLI is also the documented intent: it keeps the turn on
|
|
151
|
+
// their subscription rather than the SDK's embedded runtime.
|
|
152
|
+
env.CLAUDE_CODE_EXECUTABLE = claudePath;
|
|
153
|
+
// Least-privilege: the adapter child never talks to the dev-server's GitHub
|
|
154
|
+
// token bridge (only apps/studio/github/token.ts does), so drop the loopback
|
|
155
|
+
// keychain-bridge handle from its env. Keeps a hijacked-PATH `claude` (which
|
|
156
|
+
// would require pre-existing RCE) from reading the user's GitHub token.
|
|
157
|
+
delete env.MAUDE_TOKEN_ENDPOINT;
|
|
158
|
+
delete env.MAUDE_TOKEN_KEY;
|
|
142
159
|
// Model + effort selection — config, NOT credentials, so they're added back.
|
|
143
160
|
this.activeModel = this.desiredModel;
|
|
144
161
|
this.activeEffort = this.desiredEffort;
|
|
@@ -18,6 +18,7 @@ import DiffView from './panels/DiffView.jsx';
|
|
|
18
18
|
import GitPanel from './panels/GitPanel.jsx';
|
|
19
19
|
import IdentityBar from './panels/IdentityBar.jsx';
|
|
20
20
|
import OnboardingWizard from './panels/OnboardingWizard.jsx';
|
|
21
|
+
import { ReadinessDialog } from './panels/ReadinessList.jsx';
|
|
21
22
|
import RepoBranchSwitcher from './panels/RepoBranchSwitcher.jsx';
|
|
22
23
|
import { appIsFirstRun, isNativeApp, onUpdateReady, restartToUpdate } from './github.js';
|
|
23
24
|
import { COLLAB_TOUR } from './tour/collab-tour.js';
|
|
@@ -2249,6 +2250,7 @@ function HelpDropdown({ onAction, onClose }) {
|
|
|
2249
2250
|
// Publish → Pull cycle — a non-technical, native-app concern. A web-studio
|
|
2250
2251
|
// dev already knows git, so it's hidden there (DDR-119).
|
|
2251
2252
|
...(isNativeApp() ? [{ id: 'collab-tour', label: 'How sharing works' }] : []),
|
|
2253
|
+
...(isNativeApp() ? [{ id: 'readiness', label: 'Check AI editing readiness…' }] : []),
|
|
2252
2254
|
{ id: 'whatsnew', label: "What's new" },
|
|
2253
2255
|
]}
|
|
2254
2256
|
/>
|
|
@@ -2367,6 +2369,7 @@ function Menubar({
|
|
|
2367
2369
|
onTogglePresent,
|
|
2368
2370
|
postToActiveCanvas,
|
|
2369
2371
|
onOpenWhatsNew,
|
|
2372
|
+
onOpenReadiness,
|
|
2370
2373
|
whatsNewCount,
|
|
2371
2374
|
artboardCount = 0,
|
|
2372
2375
|
presence = null,
|
|
@@ -2630,6 +2633,7 @@ function Menubar({
|
|
|
2630
2633
|
else if (id === 'help') onOpenHelp?.();
|
|
2631
2634
|
else if (id === 'tour') onStartTour?.();
|
|
2632
2635
|
else if (id === 'collab-tour') onStartCollabTour?.();
|
|
2636
|
+
else if (id === 'readiness') onOpenReadiness?.();
|
|
2633
2637
|
else if (id === 'whatsnew') onOpenWhatsNew?.();
|
|
2634
2638
|
}}
|
|
2635
2639
|
onClose={() => setOpenMenu(null)}
|
|
@@ -5578,6 +5582,7 @@ function App() {
|
|
|
5578
5582
|
const [showHidden, setShowHidden] = useState(() => readBoolStore(SHOW_HIDDEN_STORE, false));
|
|
5579
5583
|
const [sectionsExpanded, setSectionsExpanded] = useState(() => readJsonStore(SECTIONS_STORE, {}));
|
|
5580
5584
|
const [helpOpen, setHelpOpen] = useState(false);
|
|
5585
|
+
const [readinessOpen, setReadinessOpen] = useState(false);
|
|
5581
5586
|
// ? cheat-sheet (DS components-shortcuts-overlay) — separate from the deep
|
|
5582
5587
|
// Help modal (F1), which keeps commands & flows.
|
|
5583
5588
|
const [shortcutsOpen, setShortcutsOpen] = useState(false);
|
|
@@ -7138,6 +7143,7 @@ function App() {
|
|
|
7138
7143
|
presentMode={presentMode}
|
|
7139
7144
|
onTogglePresent={togglePresent}
|
|
7140
7145
|
postToActiveCanvas={postToActiveCanvas}
|
|
7146
|
+
onOpenReadiness={() => setReadinessOpen(true)}
|
|
7141
7147
|
onOpenWhatsNew={whatsNew.openPanel}
|
|
7142
7148
|
whatsNewCount={whatsNew.unseen.length}
|
|
7143
7149
|
artboardCount={activeArtboards}
|
|
@@ -7430,6 +7436,7 @@ function App() {
|
|
|
7430
7436
|
}}
|
|
7431
7437
|
/>
|
|
7432
7438
|
<WhatsNewPanel wn={whatsNew} onStartTour={startTour} />
|
|
7439
|
+
<ReadinessDialog open={readinessOpen} onClose={() => setReadinessOpen(false)} />
|
|
7433
7440
|
{usageNudge && !tourSteps && !collabNudge && (
|
|
7434
7441
|
<div className="mdcc-tour-nudge" role="status" aria-live="polite">
|
|
7435
7442
|
<div className="mdcc-tour-nudge__body">
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
|
|
24
24
|
import { createAcpConnection, makeAcpAdapter } from './acp-runtime.js';
|
|
25
25
|
import { Markdown } from './chat-markdown.jsx';
|
|
26
|
+
import ReadinessList, { useReadiness } from './ReadinessList.jsx';
|
|
26
27
|
|
|
27
28
|
// ── inline icons (separate panel files carry their own, like GitPanel) ──
|
|
28
29
|
const Spark = ({ size = 16 }) => (
|
|
@@ -330,16 +331,18 @@ function Composer({ activeCanvas, model, setModel, effort, setEffort }) {
|
|
|
330
331
|
);
|
|
331
332
|
}
|
|
332
333
|
|
|
333
|
-
function NotConnected({ reason, claudeMissing }) {
|
|
334
|
+
function NotConnected({ reason, claudeMissing, readiness, readinessLoading, onRecheck }) {
|
|
334
335
|
return (
|
|
335
336
|
<div className="chat-disabled">
|
|
336
337
|
<span className="chat-disabled-mark">
|
|
337
338
|
<Spark size={28} />
|
|
338
339
|
</span>
|
|
339
|
-
<div className="chat-disabled-title">
|
|
340
|
+
<div className="chat-disabled-title">AI editing isn't ready yet</div>
|
|
340
341
|
<div className="chat-disabled-sub">
|
|
341
342
|
{reason ? <p>{reason}</p> : null}
|
|
342
|
-
{
|
|
343
|
+
{readiness ? (
|
|
344
|
+
<p>AI editing pairs with a Claude Code you have installed. Here's what it still needs:</p>
|
|
345
|
+
) : claudeMissing ? (
|
|
343
346
|
<p>
|
|
344
347
|
Install it with <code>npm i -g @anthropic-ai/claude-code</code>, then run{' '}
|
|
345
348
|
<code>claude</code> and <code>/login</code> in a terminal.
|
|
@@ -350,6 +353,9 @@ function NotConnected({ reason, claudeMissing }) {
|
|
|
350
353
|
</p>
|
|
351
354
|
)}
|
|
352
355
|
</div>
|
|
356
|
+
{readiness ? (
|
|
357
|
+
<ReadinessList report={readiness} loading={readinessLoading} refresh={onRecheck} />
|
|
358
|
+
) : null}
|
|
353
359
|
<div className="chat-trust">
|
|
354
360
|
<div className="chat-trust-row">
|
|
355
361
|
<Check /> Runs on your Pro/Max subscription
|
|
@@ -481,30 +487,44 @@ export default function ChatPanel({
|
|
|
481
487
|
|
|
482
488
|
// Availability is global (is claude installed) — a single probe, no connection.
|
|
483
489
|
const [status, setStatus] = useState({ available: null, reason: undefined, claudeMissing: false });
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if (alive)
|
|
490
|
+
const probeStatus = useCallback(
|
|
491
|
+
() =>
|
|
492
|
+
fetch('/_api/acp/status')
|
|
493
|
+
.then((r) => r.json())
|
|
494
|
+
.then((d) =>
|
|
490
495
|
setStatus({
|
|
491
496
|
available: d.available,
|
|
492
497
|
reason: d.reason,
|
|
493
498
|
claudeMissing: !!d.adapterEntry && !d.claudePath,
|
|
494
|
-
})
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (alive)
|
|
499
|
+
})
|
|
500
|
+
)
|
|
501
|
+
.catch(() =>
|
|
498
502
|
setStatus({
|
|
499
503
|
available: false,
|
|
500
504
|
reason: 'Could not reach the Claude bridge.',
|
|
501
505
|
claudeMissing: false,
|
|
502
|
-
})
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
506
|
+
})
|
|
507
|
+
),
|
|
508
|
+
[]
|
|
509
|
+
);
|
|
510
|
+
useEffect(() => {
|
|
511
|
+
probeStatus();
|
|
512
|
+
}, [probeStatus]);
|
|
513
|
+
|
|
514
|
+
// DDR-128 — when not connected, surface the full readiness breakdown (claude ·
|
|
515
|
+
// maude · plugins) in the not-connected explainer. Only probes when actually
|
|
516
|
+
// disconnected; Re-check re-probes BOTH the readiness report and the bridge
|
|
517
|
+
// status, so installing the missing pieces and re-checking can unlock the panel
|
|
518
|
+
// without reopening it (the persistent re-check surface).
|
|
519
|
+
const {
|
|
520
|
+
report: readiness,
|
|
521
|
+
loading: readinessLoading,
|
|
522
|
+
refresh: refreshReadiness,
|
|
523
|
+
} = useReadiness(status.available === false);
|
|
524
|
+
const recheck = useCallback(() => {
|
|
525
|
+
refreshReadiness();
|
|
526
|
+
return probeStatus();
|
|
527
|
+
}, [refreshReadiness, probeStatus]);
|
|
508
528
|
|
|
509
529
|
// Recents (for the switcher).
|
|
510
530
|
const [chats, setChats] = useState([]);
|
|
@@ -741,7 +761,13 @@ export default function ChatPanel({
|
|
|
741
761
|
) : null}
|
|
742
762
|
<div className="st-rp-body st-rp-body--chat">
|
|
743
763
|
{status.available === false ? (
|
|
744
|
-
<NotConnected
|
|
764
|
+
<NotConnected
|
|
765
|
+
reason={status.reason}
|
|
766
|
+
claudeMissing={status.claudeMissing}
|
|
767
|
+
readiness={readiness}
|
|
768
|
+
readinessLoading={readinessLoading}
|
|
769
|
+
onRecheck={recheck}
|
|
770
|
+
/>
|
|
745
771
|
) : (
|
|
746
772
|
openChatIds.map((id) => {
|
|
747
773
|
const conn = connsRef.current.get(id);
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { useEffect, useRef, useState } from 'react';
|
|
14
14
|
|
|
15
|
+
import ReadinessList, { useReadiness } from './ReadinessList.jsx';
|
|
15
16
|
import {
|
|
16
17
|
cloneRepo,
|
|
17
18
|
createProject,
|
|
@@ -143,6 +144,39 @@ function CrashOptIn() {
|
|
|
143
144
|
);
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
// AI-editing readiness (Phase 33 / DDR-128). NON-BLOCKING: the bundled core
|
|
148
|
+
// (canvas browser, version history, sharing) works with zero install, so this is a
|
|
149
|
+
// quiet, collapsible strip — never a gate. It surfaces what AI editing additionally
|
|
150
|
+
// needs (a paired Claude Code with the maude plugins + the `maude` CLI) so a missing
|
|
151
|
+
// piece is visible instead of a silent no-op when the user later types /design:edit.
|
|
152
|
+
// Stays silent until the probe answers (endpoint unreachable → render nothing).
|
|
153
|
+
function AiReadiness() {
|
|
154
|
+
const { report, loading, refresh } = useReadiness();
|
|
155
|
+
if (!report) return null;
|
|
156
|
+
const ready = report.ready;
|
|
157
|
+
return (
|
|
158
|
+
<details className="ob-readiness">
|
|
159
|
+
<summary className="ob-readiness-sum">
|
|
160
|
+
<span className={`ob-readiness-dot ob-readiness-dot--${ready ? 'ok' : 'warn'}`} aria-hidden="true" />
|
|
161
|
+
<span className="ob-readiness-lede">
|
|
162
|
+
{ready ? 'AI editing is ready' : 'AI editing needs a couple of things'}
|
|
163
|
+
</span>
|
|
164
|
+
<span className="ob-readiness-hint">
|
|
165
|
+
{ready ? 'optional — view details' : 'optional · the rest of Maude works without it'}
|
|
166
|
+
</span>
|
|
167
|
+
</summary>
|
|
168
|
+
<div className="ob-readiness-body">
|
|
169
|
+
<p className="ob-readiness-note">
|
|
170
|
+
Everything else — the canvas browser, version history, sharing — works right now. AI
|
|
171
|
+
editing additionally pairs with a Claude Code you already have installed and runs on your
|
|
172
|
+
own Pro/Max subscription.
|
|
173
|
+
</p>
|
|
174
|
+
<ReadinessList report={report} loading={loading} refresh={refresh} />
|
|
175
|
+
</div>
|
|
176
|
+
</details>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
146
180
|
// ── A · Welcome (door picker) ────────────────────────────────────────────────
|
|
147
181
|
function Welcome({ onGithub, onLocal, onHub, signing }) {
|
|
148
182
|
return (
|
|
@@ -181,6 +215,7 @@ function Welcome({ onGithub, onLocal, onHub, signing }) {
|
|
|
181
215
|
</button>
|
|
182
216
|
</div>
|
|
183
217
|
<p className="ob-foot-note">Maude never touches the terminal. Everything here happens in the app.</p>
|
|
218
|
+
<AiReadiness />
|
|
184
219
|
<CrashOptIn />
|
|
185
220
|
</main>
|
|
186
221
|
);
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// First-open AI-editing readiness list (DDR-128). Reads GET /_api/preflight and
|
|
2
|
+
// renders one row per dependency (claude · maude · plugins · agent-browser) with a
|
|
3
|
+
// status glyph, a one-line detail, and a copy-paste remediation when something's
|
|
4
|
+
// missing. Detect-and-guide only — it never installs or mutates anything.
|
|
5
|
+
//
|
|
6
|
+
// Shared by the onboarding wizard (a non-blocking strip) and the ChatPanel
|
|
7
|
+
// not-connected explainer (where a user actually hits the wall). Both also use it
|
|
8
|
+
// as the persistent re-check surface — `refresh()` re-probes without a reinstall.
|
|
9
|
+
|
|
10
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
11
|
+
|
|
12
|
+
/** Fetch + cache the readiness report. `refresh()` re-probes (the re-check button). */
|
|
13
|
+
export function useReadiness(enabled = true) {
|
|
14
|
+
const [report, setReport] = useState(null); // { ready, items } | null
|
|
15
|
+
const [loading, setLoading] = useState(enabled);
|
|
16
|
+
|
|
17
|
+
const refresh = useCallback(() => {
|
|
18
|
+
setLoading(true);
|
|
19
|
+
return fetch('/_api/preflight')
|
|
20
|
+
.then((r) => r.json())
|
|
21
|
+
.then((d) => {
|
|
22
|
+
setReport(d && Array.isArray(d.items) ? d : null);
|
|
23
|
+
setLoading(false);
|
|
24
|
+
return d;
|
|
25
|
+
})
|
|
26
|
+
.catch(() => {
|
|
27
|
+
setReport(null);
|
|
28
|
+
setLoading(false);
|
|
29
|
+
return null;
|
|
30
|
+
});
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!enabled) return undefined;
|
|
35
|
+
let alive = true;
|
|
36
|
+
fetch('/_api/preflight')
|
|
37
|
+
.then((r) => r.json())
|
|
38
|
+
.then((d) => alive && (setReport(d && Array.isArray(d.items) ? d : null), setLoading(false)))
|
|
39
|
+
.catch(() => alive && (setReport(null), setLoading(false)));
|
|
40
|
+
return () => {
|
|
41
|
+
alive = false;
|
|
42
|
+
};
|
|
43
|
+
}, [enabled]);
|
|
44
|
+
|
|
45
|
+
return { report, loading, refresh };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function StatusIcon({ status }) {
|
|
49
|
+
if (status === 'present')
|
|
50
|
+
return (
|
|
51
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
52
|
+
<polyline points="3 8.4 6.4 11.8 13 4.4" />
|
|
53
|
+
</svg>
|
|
54
|
+
);
|
|
55
|
+
if (status === 'unknown')
|
|
56
|
+
return (
|
|
57
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
58
|
+
<path d="M6 6a2 2 0 1 1 2.6 1.9c-.4.2-.6.5-.6 1V10" />
|
|
59
|
+
<circle cx="8" cy="12.4" r="0.6" fill="currentColor" stroke="none" />
|
|
60
|
+
</svg>
|
|
61
|
+
);
|
|
62
|
+
return (
|
|
63
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
64
|
+
<circle cx="8" cy="8" r="5.5" />
|
|
65
|
+
<line x1="8" y1="5" x2="8" y2="8.6" />
|
|
66
|
+
</svg>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Render `code`-fenced spans inside a string as <code>, the rest as text. */
|
|
71
|
+
function Inline({ text }) {
|
|
72
|
+
const parts = [];
|
|
73
|
+
const re = /`([^`]+)`/g;
|
|
74
|
+
let last = 0;
|
|
75
|
+
let m;
|
|
76
|
+
while ((m = re.exec(text)) !== null) {
|
|
77
|
+
if (m.index > last) parts.push(text.slice(last, m.index));
|
|
78
|
+
parts.push(<code key={parts.length}>{m[1]}</code>);
|
|
79
|
+
last = m.index + m[0].length;
|
|
80
|
+
}
|
|
81
|
+
if (last < text.length) parts.push(text.slice(last));
|
|
82
|
+
return <>{parts}</>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function Row({ item }) {
|
|
86
|
+
const [copied, setCopied] = useState(false);
|
|
87
|
+
const copyFix = () => {
|
|
88
|
+
const cmds = item.remediation?.replace(/`/g, '') ?? '';
|
|
89
|
+
navigator.clipboard?.writeText(cmds).then(
|
|
90
|
+
() => {
|
|
91
|
+
setCopied(true);
|
|
92
|
+
setTimeout(() => setCopied(false), 1500);
|
|
93
|
+
},
|
|
94
|
+
() => {}
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
return (
|
|
98
|
+
<li className={`rdy-row rdy-row--${item.status}${item.required ? '' : ' rdy-row--opt'}`}>
|
|
99
|
+
<span className="rdy-ic" aria-hidden="true">
|
|
100
|
+
<StatusIcon status={item.status} />
|
|
101
|
+
</span>
|
|
102
|
+
<span className="rdy-tx">
|
|
103
|
+
<span className="rdy-label">{item.label}</span>
|
|
104
|
+
<span className="rdy-detail">
|
|
105
|
+
<Inline text={item.detail} />
|
|
106
|
+
</span>
|
|
107
|
+
{item.remediation ? (
|
|
108
|
+
<span className="rdy-fix">
|
|
109
|
+
<span className="rdy-fix-tx">
|
|
110
|
+
<Inline text={item.remediation} />
|
|
111
|
+
</span>
|
|
112
|
+
<button type="button" className="rdy-copy" onClick={copyFix} aria-label={`Copy the fix for ${item.label}`}>
|
|
113
|
+
{copied ? 'Copied' : 'Copy'}
|
|
114
|
+
</button>
|
|
115
|
+
</span>
|
|
116
|
+
) : null}
|
|
117
|
+
</span>
|
|
118
|
+
</li>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default function ReadinessList({ report, loading, refresh }) {
|
|
123
|
+
return (
|
|
124
|
+
<div className="rdy">
|
|
125
|
+
<ul className="rdy-list">
|
|
126
|
+
{report?.items?.map((it) => (
|
|
127
|
+
<Row key={it.id} item={it} />
|
|
128
|
+
))}
|
|
129
|
+
</ul>
|
|
130
|
+
{refresh ? (
|
|
131
|
+
<div className="rdy-foot">
|
|
132
|
+
<button type="button" className="btn btn--ghost btn--sm rdy-recheck" onClick={refresh} disabled={loading}>
|
|
133
|
+
{loading ? 'Checking…' : 'Re-check'}
|
|
134
|
+
</button>
|
|
135
|
+
</div>
|
|
136
|
+
) : null}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Standalone readiness modal reachable any time from Help ▸ Check AI editing
|
|
142
|
+
// readiness… (DDR-128, T6 — the persistent re-check surface, not gated on
|
|
143
|
+
// first-run or a disconnected chat). Reuses the shared help-modal chrome (backdrop
|
|
144
|
+
// + header + body) so it matches the What's New / Help dialogs, and the same
|
|
145
|
+
// ReadinessList. Self-contained: probes only while open (the hook's `enabled` gate),
|
|
146
|
+
// re-probes via the list's Re-check button.
|
|
147
|
+
export function ReadinessDialog({ open, onClose }) {
|
|
148
|
+
const { report, loading, refresh } = useReadiness(open);
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!open) return undefined;
|
|
151
|
+
const onKey = (e) => e.key === 'Escape' && onClose();
|
|
152
|
+
window.addEventListener('keydown', onKey);
|
|
153
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
154
|
+
}, [open, onClose]);
|
|
155
|
+
if (!open) return null;
|
|
156
|
+
return (
|
|
157
|
+
<div
|
|
158
|
+
className="help-modal-backdrop"
|
|
159
|
+
role="presentation"
|
|
160
|
+
onMouseDown={(e) => {
|
|
161
|
+
if (e.target === e.currentTarget) onClose();
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
<div className="help-modal rdy-modal" role="dialog" aria-modal="true" aria-labelledby="rdy-modal-title">
|
|
165
|
+
<header className="help-modal-hd">
|
|
166
|
+
<span className="title" id="rdy-modal-title">
|
|
167
|
+
Check AI editing readiness
|
|
168
|
+
</span>
|
|
169
|
+
<button type="button" className="help-modal-close" aria-label="Close (Esc)" onClick={onClose}>
|
|
170
|
+
×
|
|
171
|
+
</button>
|
|
172
|
+
</header>
|
|
173
|
+
<div className="help-modal-body">
|
|
174
|
+
<p className="rdy-modal-note">
|
|
175
|
+
The canvas browser, version history, and sharing work with no setup. AI editing
|
|
176
|
+
additionally drives a Claude Code you have installed — here's what it needs:
|
|
177
|
+
</p>
|
|
178
|
+
{report ? (
|
|
179
|
+
<ReadinessList report={report} loading={loading} refresh={refresh} />
|
|
180
|
+
) : (
|
|
181
|
+
<p className="rdy-modal-note">
|
|
182
|
+
{loading ? 'Checking…' : "Couldn't reach the readiness probe."}
|
|
183
|
+
</p>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
@@ -1617,6 +1617,18 @@ body.st-scrubbing, body.st-scrubbing * { cursor: ew-resize !important; user-sele
|
|
|
1617
1617
|
.ob-crash-optin { display: flex; align-items: flex-start; gap: var(--space-2); max-width: 560px; margin: var(--space-3) 0 0; color: var(--fg-2); font-size: var(--type-xs); line-height: var(--lh-md); cursor: pointer; text-align: left; }
|
|
1618
1618
|
.ob-crash-optin input { margin-top: 2px; flex: 0 0 auto; accent-color: var(--accent); cursor: pointer; }
|
|
1619
1619
|
|
|
1620
|
+
/* AI-editing readiness strip (Phase 33 / DDR-128) — non-blocking, collapsible. */
|
|
1621
|
+
.ob-readiness { max-width: 560px; margin: var(--space-3) 0 0; border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); background: var(--bg-1); text-align: left; }
|
|
1622
|
+
.ob-readiness-sum { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-3); cursor: pointer; list-style: none; font-size: var(--type-sm); color: var(--fg-1); }
|
|
1623
|
+
.ob-readiness-sum::-webkit-details-marker { display: none; }
|
|
1624
|
+
.ob-readiness-dot { flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%; }
|
|
1625
|
+
.ob-readiness-dot--ok { background: var(--status-success); }
|
|
1626
|
+
.ob-readiness-dot--warn { background: var(--status-warning, var(--status-info)); }
|
|
1627
|
+
.ob-readiness-lede { font-weight: 600; color: var(--fg-0); }
|
|
1628
|
+
.ob-readiness-hint { margin-left: auto; color: var(--fg-3); font-size: var(--type-xs); }
|
|
1629
|
+
.ob-readiness-body { padding: 0 var(--space-3) var(--space-3); display: flex; flex-direction: column; gap: var(--space-3); }
|
|
1630
|
+
.ob-readiness-note { margin: 0; color: var(--fg-2); font-size: var(--type-xs); line-height: var(--lh-md); }
|
|
1631
|
+
|
|
1620
1632
|
.ob-create { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: var(--space-4); width: 100%; max-width: 720px; text-align: left; padding: var(--space-5); border: 1px solid color-mix(in oklab, var(--accent) 45%, transparent); border-radius: var(--radius-lg); background: linear-gradient(180deg, var(--accent-tint) 0%, transparent 80%), var(--bg-1); cursor: pointer; color: inherit; transition: border-color var(--dur-soft) var(--ease-out); }
|
|
1621
1633
|
.ob-create:hover { border-color: var(--accent); }
|
|
1622
1634
|
.ob-create:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
@@ -769,3 +769,53 @@
|
|
|
769
769
|
}
|
|
770
770
|
}
|
|
771
771
|
}
|
|
772
|
+
|
|
773
|
+
/* DDR-128 — AI-editing readiness list (ReadinessList.jsx). Shared by the onboarding
|
|
774
|
+
readiness strip and the ChatPanel not-connected explainer. Detect-and-guide:
|
|
775
|
+
present rows reassure, missing rows carry a copy-paste fix, optional rows stay
|
|
776
|
+
muted (never alarming). */
|
|
777
|
+
@layer components {
|
|
778
|
+
.rdy { display: flex; flex-direction: column; gap: var(--space-3); }
|
|
779
|
+
.rdy-list {
|
|
780
|
+
list-style: none; margin: 0; padding: 0;
|
|
781
|
+
display: flex; flex-direction: column; gap: var(--space-2);
|
|
782
|
+
}
|
|
783
|
+
.rdy-row {
|
|
784
|
+
display: flex; gap: var(--space-2); align-items: flex-start;
|
|
785
|
+
padding: var(--space-2) var(--space-3);
|
|
786
|
+
border: 1px solid var(--border-subtle); border-radius: var(--radius-sm);
|
|
787
|
+
background: var(--bg-2); text-align: left;
|
|
788
|
+
}
|
|
789
|
+
.rdy-ic { flex-shrink: 0; margin-top: 1px; }
|
|
790
|
+
.rdy-row--present .rdy-ic { color: var(--status-success); }
|
|
791
|
+
.rdy-row--missing .rdy-ic { color: var(--status-error); }
|
|
792
|
+
.rdy-row--unknown .rdy-ic { color: var(--status-info); }
|
|
793
|
+
/* optional, missing → muted, not alarming */
|
|
794
|
+
.rdy-row--opt.rdy-row--missing .rdy-ic { color: var(--fg-3); }
|
|
795
|
+
.rdy-tx { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
796
|
+
.rdy-label { font-size: var(--type-sm); font-weight: 600; color: var(--fg-0); }
|
|
797
|
+
.rdy-detail { font-size: var(--type-xs); color: var(--fg-2); line-height: var(--lh-xs); }
|
|
798
|
+
.rdy-fix {
|
|
799
|
+
margin-top: var(--space-2); display: flex; gap: var(--space-2);
|
|
800
|
+
align-items: flex-start;
|
|
801
|
+
font-size: var(--type-xs); color: var(--fg-1); line-height: var(--lh-xs);
|
|
802
|
+
}
|
|
803
|
+
.rdy-fix-tx { min-width: 0; }
|
|
804
|
+
.rdy code {
|
|
805
|
+
font-family: var(--font-mono); font-size: var(--type-xs);
|
|
806
|
+
color: var(--fg-0); background: var(--bg-3);
|
|
807
|
+
padding: 1px 5px; border-radius: var(--radius-xs);
|
|
808
|
+
overflow-wrap: anywhere;
|
|
809
|
+
}
|
|
810
|
+
.rdy-copy {
|
|
811
|
+
flex-shrink: 0; cursor: pointer;
|
|
812
|
+
font-size: var(--type-xs); color: var(--fg-2);
|
|
813
|
+
background: transparent; border: 1px solid var(--border-subtle);
|
|
814
|
+
border-radius: var(--radius-xs); padding: 1px 6px;
|
|
815
|
+
}
|
|
816
|
+
.rdy-copy:hover { color: var(--fg-0); background: var(--bg-3); }
|
|
817
|
+
.rdy-foot { display: flex; justify-content: flex-end; }
|
|
818
|
+
/* Help ▸ Check AI editing readiness… modal (reuses .help-modal chrome). */
|
|
819
|
+
.rdy-modal { max-width: 520px; }
|
|
820
|
+
.rdy-modal-note { margin: 0 0 var(--space-3); color: var(--fg-2); font-size: var(--type-sm); line-height: var(--lh-md); }
|
|
821
|
+
}
|