@gmickel/gno 1.30.7 → 1.32.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.
- package/README.md +6 -5
- package/assets/skill/SKILL.md +25 -0
- package/assets/skill/mcp-reference.md +6 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.30.7.zip → gno-browser-clipper-v1.32.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.32.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +19 -0
- package/spec/db/schema.sql +55 -0
- package/spec/mcp.md +176 -11
- package/spec/output-schemas/file-refactor-apply-result.schema.json +305 -0
- package/spec/output-schemas/file-refactor-preview.schema.json +393 -0
- package/spec/output-schemas/section-target-create-result.schema.json +20 -0
- package/spec/output-schemas/section-target-resolve-result.schema.json +194 -0
- package/spec/output-schemas/section-target.schema.json +118 -0
- package/spec/output-schemas/section.schema.json +113 -0
- package/src/core/document-capabilities.ts +13 -0
- package/src/core/file-ops.ts +129 -1
- package/src/core/file-refactor-adapter.ts +329 -0
- package/src/core/file-refactor-apply-edits.ts +61 -0
- package/src/core/file-refactor-apply-fs.ts +512 -0
- package/src/core/file-refactor-apply-safety.ts +340 -0
- package/src/core/file-refactor-apply-validate.ts +401 -0
- package/src/core/file-refactor-contract.ts +486 -0
- package/src/core/file-refactor-destination.ts +123 -0
- package/src/core/file-refactor-from-snapshot.ts +148 -0
- package/src/core/file-refactor-journal-port.ts +150 -0
- package/src/core/file-refactor-journal.ts +347 -0
- package/src/core/file-refactor-paths.ts +60 -0
- package/src/core/file-refactor-plan-classify.ts +208 -0
- package/src/core/file-refactor-plan-validate.ts +169 -0
- package/src/core/file-refactor-planner-types.ts +62 -0
- package/src/core/file-refactor-planner.ts +423 -0
- package/src/core/file-refactor-resolve.ts +280 -0
- package/src/core/file-refactor-service.ts +468 -0
- package/src/core/file-refactors.ts +84 -56
- package/src/core/link-destination-parse.ts +275 -0
- package/src/core/link-inventory-markdown.ts +454 -0
- package/src/core/link-inventory-opaque.ts +244 -0
- package/src/core/link-inventory-types.ts +47 -0
- package/src/core/link-inventory.ts +182 -0
- package/src/core/link-relevance.ts +150 -0
- package/src/core/section-parse.ts +187 -0
- package/src/core/section-target-link.ts +154 -0
- package/src/core/section-target-resolve.ts +351 -0
- package/src/core/section-target-transport.ts +519 -0
- package/src/core/section-target.ts +263 -0
- package/src/core/sections.ts +60 -115
- package/src/mcp/AGENTS.md +1 -0
- package/src/mcp/CLAUDE.md +1 -0
- package/src/mcp/http-egress.ts +1 -0
- package/src/mcp/tools/index.ts +37 -17
- package/src/mcp/tools/sections.ts +512 -0
- package/src/mcp/tools/workspace-write.ts +215 -97
- package/src/sdk/client.ts +238 -116
- package/src/sdk/index.ts +12 -0
- package/src/sdk/types.ts +61 -3
- package/src/serve/file-refactor-http.ts +239 -0
- package/src/serve/public/components/RefactorImpactPreview.tsx +227 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/lib/section-links.ts +189 -0
- package/src/serve/public/pages/DocView.tsx +395 -77
- package/src/serve/routes/api.ts +191 -104
- package/src/serve/routes/section-targets.ts +221 -0
- package/src/serve/server.ts +34 -0
- package/src/store/migrations/026-file-refactor-recovery-journal.ts +72 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +452 -0
- package/src/store/sqlite/file-refactor-journal-store.ts +275 -0
- package/src/store/types.ts +84 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.30.7.zip.sha256 +0 -1
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST transport adapter for canonical reference-safe rename/move.
|
|
3
|
+
*
|
|
4
|
+
* Handlers must not implement link rewrite logic — they only build
|
|
5
|
+
* targets, call the core planner/apply service, and map typed results.
|
|
6
|
+
*
|
|
7
|
+
* @module src/serve/file-refactor-http
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
FileRefactorApplyResult,
|
|
12
|
+
FileRefactorPreviewPlan,
|
|
13
|
+
FileRefactorReasonCode,
|
|
14
|
+
} from "../core/file-refactors";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
applyCanonicalFileRefactor,
|
|
18
|
+
buildCanonicalRefactorPlan,
|
|
19
|
+
buildDurableFileRefactorApplyDeps,
|
|
20
|
+
FILE_REFACTOR_APPLY_CONFIRMATION,
|
|
21
|
+
FILE_REFACTOR_SCHEMA_VERSION,
|
|
22
|
+
parseRefactorApplyConfirmation as parseCoreRefactorApplyConfirmation,
|
|
23
|
+
resolveMoveTarget,
|
|
24
|
+
resolveRenameTarget,
|
|
25
|
+
} from "../core/file-refactors";
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
applyCanonicalFileRefactor,
|
|
29
|
+
buildCanonicalRefactorPlan,
|
|
30
|
+
buildDurableFileRefactorApplyDeps as buildFileRefactorApplyDeps,
|
|
31
|
+
resolveMoveTarget,
|
|
32
|
+
resolveRenameTarget,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export interface FileRefactorHttpError {
|
|
36
|
+
code: string;
|
|
37
|
+
message: string;
|
|
38
|
+
status: number;
|
|
39
|
+
details?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface FileRefactorApplyHttpSuccess {
|
|
43
|
+
success: true;
|
|
44
|
+
uri: string;
|
|
45
|
+
path: string;
|
|
46
|
+
relPath: string;
|
|
47
|
+
planDigest: string;
|
|
48
|
+
status: FileRefactorApplyResult["status"];
|
|
49
|
+
apply: FileRefactorApplyResult;
|
|
50
|
+
refactorWarnings: {
|
|
51
|
+
warnings: string[];
|
|
52
|
+
backlinkCount: number;
|
|
53
|
+
wikiLinkCount: number;
|
|
54
|
+
markdownLinkCount: number;
|
|
55
|
+
};
|
|
56
|
+
warning?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function warningsFromPlan(plan: FileRefactorPreviewPlan) {
|
|
60
|
+
return {
|
|
61
|
+
warnings: [...plan.safety.warnings],
|
|
62
|
+
backlinkCount: plan.safety.backlinkCount,
|
|
63
|
+
wikiLinkCount: plan.safety.wikiLinkCount,
|
|
64
|
+
markdownLinkCount: plan.safety.markdownLinkCount,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function toRefactorPlanResponse(plan: FileRefactorPreviewPlan) {
|
|
69
|
+
return {
|
|
70
|
+
...plan,
|
|
71
|
+
operation: plan.operation,
|
|
72
|
+
nextRelPath: plan.target.relPath,
|
|
73
|
+
nextUri: plan.target.uri,
|
|
74
|
+
refactorWarnings: warningsFromPlan(plan),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function reasonMessage(
|
|
79
|
+
status: FileRefactorApplyResult["status"],
|
|
80
|
+
reasonCode?: FileRefactorReasonCode
|
|
81
|
+
): string {
|
|
82
|
+
switch (status) {
|
|
83
|
+
case "stale_plan":
|
|
84
|
+
return "This refactor plan is stale. Preview again, then confirm the exact plan digest.";
|
|
85
|
+
case "conflict":
|
|
86
|
+
return "Another workspace mutation is in progress or the target is unsafe. Retry after it finishes.";
|
|
87
|
+
case "unsupported":
|
|
88
|
+
if (
|
|
89
|
+
reasonCode === "capability_denied" ||
|
|
90
|
+
reasonCode === "read_only_document"
|
|
91
|
+
) {
|
|
92
|
+
return "This document cannot be refactored in place from GNO.";
|
|
93
|
+
}
|
|
94
|
+
if (reasonCode === "occupied_target") {
|
|
95
|
+
return "A file with that name already exists at the destination";
|
|
96
|
+
}
|
|
97
|
+
return reasonCode
|
|
98
|
+
? `Refactor cannot be applied (${reasonCode}).`
|
|
99
|
+
: "Refactor cannot be applied.";
|
|
100
|
+
case "failed_rolled_back":
|
|
101
|
+
if (reasonCode === "rollback_recovery_required") {
|
|
102
|
+
return "Refactor failed and needs recovery. Filesystem state may require manual inspection using the recovery journal id.";
|
|
103
|
+
}
|
|
104
|
+
return "Refactor failed and was rolled back. No partial file set was left behind.";
|
|
105
|
+
default:
|
|
106
|
+
return "Refactor failed.";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function mapApplyResultToHttpError(
|
|
111
|
+
result: FileRefactorApplyResult
|
|
112
|
+
): FileRefactorHttpError {
|
|
113
|
+
const reasonCode = "reasonCode" in result ? result.reasonCode : undefined;
|
|
114
|
+
const details: Record<string, unknown> = {
|
|
115
|
+
planDigest: result.planDigest,
|
|
116
|
+
status: result.status,
|
|
117
|
+
filesystem: result.filesystem,
|
|
118
|
+
indexConvergence: result.indexConvergence,
|
|
119
|
+
};
|
|
120
|
+
if (reasonCode) {
|
|
121
|
+
details.reasonCode = reasonCode;
|
|
122
|
+
}
|
|
123
|
+
if (result.filesystem.recoveryJournalId) {
|
|
124
|
+
details.recoveryJournalId = result.filesystem.recoveryJournalId;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (result.status === "stale_plan") {
|
|
128
|
+
return {
|
|
129
|
+
code: "STALE_PLAN",
|
|
130
|
+
message: reasonMessage(result.status, reasonCode),
|
|
131
|
+
status: 409,
|
|
132
|
+
details,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (result.status === "conflict") {
|
|
136
|
+
return {
|
|
137
|
+
code: "CONFLICT",
|
|
138
|
+
message: reasonMessage(result.status, reasonCode),
|
|
139
|
+
status: 409,
|
|
140
|
+
details,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (result.status === "unsupported") {
|
|
144
|
+
if (
|
|
145
|
+
reasonCode === "capability_denied" ||
|
|
146
|
+
reasonCode === "read_only_document"
|
|
147
|
+
) {
|
|
148
|
+
return {
|
|
149
|
+
code: "READ_ONLY",
|
|
150
|
+
message: reasonMessage(result.status, reasonCode),
|
|
151
|
+
status: 409,
|
|
152
|
+
details,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (reasonCode === "occupied_target") {
|
|
156
|
+
return {
|
|
157
|
+
code: "CONFLICT",
|
|
158
|
+
message: reasonMessage(result.status, reasonCode),
|
|
159
|
+
status: 409,
|
|
160
|
+
details,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
code: "UNSUPPORTED",
|
|
165
|
+
message: reasonMessage(result.status, reasonCode),
|
|
166
|
+
status: 409,
|
|
167
|
+
details,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (result.status === "failed_rolled_back") {
|
|
171
|
+
const recovery =
|
|
172
|
+
result.filesystem.state === "recovery_required" ||
|
|
173
|
+
reasonCode === "rollback_recovery_required";
|
|
174
|
+
return {
|
|
175
|
+
code: recovery ? "RECOVERY_REQUIRED" : "ROLLED_BACK",
|
|
176
|
+
message: reasonMessage(result.status, reasonCode),
|
|
177
|
+
status: 500,
|
|
178
|
+
details,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
code: "RUNTIME",
|
|
184
|
+
message: reasonMessage(result.status, reasonCode),
|
|
185
|
+
status: 500,
|
|
186
|
+
details,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function mapApplyResultToHttpSuccess(input: {
|
|
191
|
+
plan: FileRefactorPreviewPlan;
|
|
192
|
+
result: FileRefactorApplyResult;
|
|
193
|
+
targetFullPath: string;
|
|
194
|
+
operationLabel: "renamed" | "moved";
|
|
195
|
+
}): FileRefactorApplyHttpSuccess | FileRefactorHttpError {
|
|
196
|
+
const { plan, result, targetFullPath, operationLabel } = input;
|
|
197
|
+
|
|
198
|
+
if (
|
|
199
|
+
result.status !== "applied" &&
|
|
200
|
+
result.status !== "applied_with_sync_pending"
|
|
201
|
+
) {
|
|
202
|
+
return mapApplyResultToHttpError(result);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const warning =
|
|
206
|
+
result.status === "applied_with_sync_pending"
|
|
207
|
+
? `File ${operationLabel} on disk, but index refresh failed. Run Update All to reconcile the workspace.`
|
|
208
|
+
: undefined;
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
success: true,
|
|
212
|
+
uri: plan.target.uri,
|
|
213
|
+
path: targetFullPath,
|
|
214
|
+
relPath: plan.target.relPath,
|
|
215
|
+
planDigest: result.planDigest,
|
|
216
|
+
status: result.status,
|
|
217
|
+
apply: result,
|
|
218
|
+
refactorWarnings: warningsFromPlan(plan),
|
|
219
|
+
warning,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface ParsedRefactorApplyConfirmation {
|
|
224
|
+
planDigest: string;
|
|
225
|
+
confirmation: typeof FILE_REFACTOR_APPLY_CONFIRMATION;
|
|
226
|
+
schemaVersion: typeof FILE_REFACTOR_SCHEMA_VERSION;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function parseRefactorApplyConfirmation(body: {
|
|
230
|
+
planDigest?: unknown;
|
|
231
|
+
confirmation?: unknown;
|
|
232
|
+
schemaVersion?: unknown;
|
|
233
|
+
}): ParsedRefactorApplyConfirmation | FileRefactorHttpError {
|
|
234
|
+
const parsed = parseCoreRefactorApplyConfirmation(body);
|
|
235
|
+
if ("error" in parsed) {
|
|
236
|
+
return { code: "VALIDATION", message: parsed.message, status: 400 };
|
|
237
|
+
}
|
|
238
|
+
return parsed;
|
|
239
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical refactor impact preview for rename/move dialogs.
|
|
3
|
+
* Scholarly Dusk vocabulary: rail-style labels, mono paths, amber caution.
|
|
4
|
+
*
|
|
5
|
+
* @module src/serve/public/components/RefactorImpactPreview
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FileRefactorPreviewPlan } from "../../../core/file-refactor-contract";
|
|
9
|
+
|
|
10
|
+
const UNRESOLVED_CLASSIFICATIONS = new Set([
|
|
11
|
+
"ambiguous",
|
|
12
|
+
"unsupported",
|
|
13
|
+
"malformed",
|
|
14
|
+
"invalid",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function shortDigest(digest: string): string {
|
|
18
|
+
return `${digest.slice(0, 8)}…${digest.slice(-6)}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RefactorImpactPreviewProps {
|
|
22
|
+
plan: FileRefactorPreviewPlan | null;
|
|
23
|
+
loading?: boolean;
|
|
24
|
+
confirmed: boolean;
|
|
25
|
+
onConfirmedChange: (confirmed: boolean) => void;
|
|
26
|
+
/** Accessible outcome message after apply (sync-pending, recovery, etc.). */
|
|
27
|
+
outcomeMessage?: string | null;
|
|
28
|
+
outcomeTone?: "warning" | "error" | "success";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function RefactorImpactPreview({
|
|
32
|
+
plan,
|
|
33
|
+
loading = false,
|
|
34
|
+
confirmed,
|
|
35
|
+
onConfirmedChange,
|
|
36
|
+
outcomeMessage,
|
|
37
|
+
outcomeTone = "warning",
|
|
38
|
+
}: RefactorImpactPreviewProps) {
|
|
39
|
+
if (loading && !plan) {
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
aria-busy="true"
|
|
43
|
+
aria-live="polite"
|
|
44
|
+
className="rounded-md border border-border/30 bg-muted/20 px-3 py-2 font-mono text-[11px] text-muted-foreground"
|
|
45
|
+
>
|
|
46
|
+
Computing reference impact…
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!plan) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const rewriteable = plan.examinedReferences.filter(
|
|
56
|
+
(ref) => ref.classification === "rewriteable"
|
|
57
|
+
);
|
|
58
|
+
const unresolved = plan.examinedReferences.filter((ref) =>
|
|
59
|
+
UNRESOLVED_CLASSIFICATIONS.has(ref.classification)
|
|
60
|
+
);
|
|
61
|
+
const affectedPaths = plan.affectedDocuments.map((doc) => doc.relPath);
|
|
62
|
+
const confirmId = `refactor-confirm-${plan.planDigest.slice(0, 12)}`;
|
|
63
|
+
|
|
64
|
+
const outcomeClass =
|
|
65
|
+
outcomeTone === "error"
|
|
66
|
+
? "border-destructive/40 bg-destructive/10 text-destructive"
|
|
67
|
+
: outcomeTone === "success"
|
|
68
|
+
? "border-primary/30 bg-primary/10 text-primary"
|
|
69
|
+
: "border-amber-500/30 bg-amber-500/10 text-amber-500";
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="max-h-[40vh] space-y-3 overflow-y-auto pr-1">
|
|
73
|
+
<div className="space-y-1">
|
|
74
|
+
<div className="font-mono text-[10px] text-muted-foreground/60 uppercase tracking-[0.15em]">
|
|
75
|
+
Safety
|
|
76
|
+
</div>
|
|
77
|
+
<div
|
|
78
|
+
aria-live="polite"
|
|
79
|
+
className={
|
|
80
|
+
plan.canApply
|
|
81
|
+
? "rounded-md border border-primary/25 bg-primary/10 px-3 py-2 text-[13px] text-primary"
|
|
82
|
+
: "rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-[13px] text-destructive"
|
|
83
|
+
}
|
|
84
|
+
role="status"
|
|
85
|
+
>
|
|
86
|
+
{plan.canApply
|
|
87
|
+
? "Plan is safe to apply with the exact digest below."
|
|
88
|
+
: `Cannot apply: ${(plan.safety.blockingReasonCodes[0] ?? "blocked").replaceAll("_", " ")}.`}
|
|
89
|
+
</div>
|
|
90
|
+
<p className="font-mono text-[11px] text-muted-foreground/70 break-all">
|
|
91
|
+
Digest {shortDigest(plan.planDigest)}
|
|
92
|
+
</p>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div className="space-y-1">
|
|
96
|
+
<div className="font-mono text-[10px] text-muted-foreground/60 uppercase tracking-[0.15em]">
|
|
97
|
+
Target
|
|
98
|
+
</div>
|
|
99
|
+
<p className="font-mono text-[11px] text-muted-foreground/80 break-all">
|
|
100
|
+
{plan.source.relPath} → {plan.target.relPath}
|
|
101
|
+
</p>
|
|
102
|
+
<p className="font-mono text-[10px] text-muted-foreground/50 tabular-nums">
|
|
103
|
+
{plan.safety.rewriteableCount} rewrite
|
|
104
|
+
{plan.safety.rewriteableCount === 1 ? "" : "s"} ·{" "}
|
|
105
|
+
{plan.safety.unchangedCount} unchanged · {unresolved.length}{" "}
|
|
106
|
+
unresolved
|
|
107
|
+
</p>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{affectedPaths.length > 0 && (
|
|
111
|
+
<div className="space-y-1">
|
|
112
|
+
<div className="font-mono text-[10px] text-muted-foreground/60 uppercase tracking-[0.15em]">
|
|
113
|
+
Affected documents
|
|
114
|
+
</div>
|
|
115
|
+
<ul className="space-y-0.5">
|
|
116
|
+
{affectedPaths.map((path) => (
|
|
117
|
+
<li
|
|
118
|
+
className="font-mono text-[11px] text-muted-foreground/80 break-all"
|
|
119
|
+
key={path}
|
|
120
|
+
>
|
|
121
|
+
{path}
|
|
122
|
+
</li>
|
|
123
|
+
))}
|
|
124
|
+
</ul>
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{rewriteable.length > 0 && (
|
|
129
|
+
<div className="space-y-1">
|
|
130
|
+
<div className="font-mono text-[10px] text-muted-foreground/60 uppercase tracking-[0.15em]">
|
|
131
|
+
Rewrites
|
|
132
|
+
</div>
|
|
133
|
+
<ul className="space-y-1">
|
|
134
|
+
{rewriteable.slice(0, 12).map((ref) => (
|
|
135
|
+
<li
|
|
136
|
+
className="rounded-sm px-2 py-1 font-mono text-[11px] text-muted-foreground hover:bg-muted/20"
|
|
137
|
+
key={`${ref.documentRelPath}:${ref.startLine ?? 0}:${ref.startCol ?? 0}:${ref.originalDestination ?? ""}`}
|
|
138
|
+
>
|
|
139
|
+
<span className="text-foreground/90">
|
|
140
|
+
{ref.documentRelPath}
|
|
141
|
+
</span>
|
|
142
|
+
<span className="text-muted-foreground/50"> · </span>
|
|
143
|
+
<span>
|
|
144
|
+
{ref.originalDestination ?? "?"} →{" "}
|
|
145
|
+
{ref.proposedDestination ?? "?"}
|
|
146
|
+
</span>
|
|
147
|
+
</li>
|
|
148
|
+
))}
|
|
149
|
+
{rewriteable.length > 12 && (
|
|
150
|
+
<li className="font-mono text-[10px] text-muted-foreground/50">
|
|
151
|
+
+{rewriteable.length - 12} more
|
|
152
|
+
</li>
|
|
153
|
+
)}
|
|
154
|
+
</ul>
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{unresolved.length > 0 && (
|
|
159
|
+
<div className="space-y-1">
|
|
160
|
+
<div className="font-mono text-[10px] text-amber-500/70 uppercase tracking-[0.15em]">
|
|
161
|
+
Unresolved / unsupported
|
|
162
|
+
</div>
|
|
163
|
+
<ul className="space-y-1">
|
|
164
|
+
{unresolved.slice(0, 12).map((ref) => (
|
|
165
|
+
<li
|
|
166
|
+
className="rounded-sm border border-amber-500/20 bg-amber-500/5 px-2 py-1 font-mono text-[11px] text-amber-500/90"
|
|
167
|
+
key={`${ref.documentRelPath}:${ref.classification}:${ref.startLine ?? 0}:${ref.originalDestination ?? ""}`}
|
|
168
|
+
>
|
|
169
|
+
<span>
|
|
170
|
+
{ref.documentRelPath}
|
|
171
|
+
{ref.startLine ? `:${ref.startLine}` : ""}
|
|
172
|
+
</span>
|
|
173
|
+
<span className="opacity-60"> · {ref.classification}</span>
|
|
174
|
+
{ref.reasonCode && (
|
|
175
|
+
<span className="opacity-60"> · {ref.reasonCode}</span>
|
|
176
|
+
)}
|
|
177
|
+
</li>
|
|
178
|
+
))}
|
|
179
|
+
{unresolved.length > 12 && (
|
|
180
|
+
<li className="font-mono text-[10px] text-amber-500/60">
|
|
181
|
+
+{unresolved.length - 12} more
|
|
182
|
+
</li>
|
|
183
|
+
)}
|
|
184
|
+
</ul>
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
|
|
188
|
+
{plan.safety.warnings.length > 0 && (
|
|
189
|
+
<div
|
|
190
|
+
className="rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-amber-500 text-sm"
|
|
191
|
+
role="status"
|
|
192
|
+
>
|
|
193
|
+
{plan.safety.warnings.map((warning) => (
|
|
194
|
+
<div key={warning}>{warning}</div>
|
|
195
|
+
))}
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
<div className="flex items-start gap-2 rounded-md border border-border/30 bg-muted/15 px-3 py-2">
|
|
200
|
+
<input
|
|
201
|
+
checked={confirmed}
|
|
202
|
+
className="mt-0.5 size-4 accent-primary"
|
|
203
|
+
disabled={!plan.canApply}
|
|
204
|
+
id={confirmId}
|
|
205
|
+
onChange={(event) => onConfirmedChange(event.target.checked)}
|
|
206
|
+
type="checkbox"
|
|
207
|
+
/>
|
|
208
|
+
<label className="text-[13px] leading-snug" htmlFor={confirmId}>
|
|
209
|
+
Confirm exact plan digest{" "}
|
|
210
|
+
<span className="font-mono text-[11px] text-muted-foreground">
|
|
211
|
+
{shortDigest(plan.planDigest)}
|
|
212
|
+
</span>
|
|
213
|
+
</label>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
{outcomeMessage && (
|
|
217
|
+
<div
|
|
218
|
+
aria-live="assertive"
|
|
219
|
+
className={`rounded-md border px-3 py-2 text-sm ${outcomeClass}`}
|
|
220
|
+
role="alert"
|
|
221
|
+
>
|
|
222
|
+
{outcomeMessage}
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
}
|