@uniqbit/mate-core 0.18.5 → 1.0.0-canary.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/claude-plugin/hooks/companion-instructions.mjs +9 -0
- package/claude-plugin/hooks/hooks.json +12 -2
- package/package.json +10 -2
- package/src/brand/art.generated.ts +69 -0
- package/src/brand/index.ts +12 -0
- package/src/brand/palette.ts +19 -0
- package/src/brand/svg.generated.ts +8 -0
- package/src/brand/terminal.ts +83 -0
- package/src/brand/types.ts +33 -0
- package/src/cli/commands/artifact/artifact.ts +8 -0
- package/src/cli/commands/artifact/delivery/archive.ts +117 -0
- package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
- package/src/cli/commands/artifact/delivery/environment.ts +104 -0
- package/src/cli/commands/artifact/delivery/link.ts +154 -0
- package/src/cli/commands/artifact/delivery/output.ts +58 -0
- package/src/cli/commands/artifact/finish/command.ts +33 -3
- package/src/cli/commands/artifact/finish/engine.ts +47 -3
- package/src/cli/commands/artifact/finish/finisher.ts +10 -0
- package/src/cli/commands/artifact/finish/openspec.ts +177 -138
- package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
- package/src/cli/commands/artifact/pending/command.ts +31 -3
- package/src/cli/commands/artifact/pending/discovery.ts +21 -57
- package/src/cli/commands/cap/graphify.ts +52 -3
- package/src/cli/commands/cap/index-cmd.ts +8 -1
- package/src/cli/commands/cap/openspec.ts +13 -25
- package/src/cli/commands/companion/open.ts +56 -39
- package/src/cli/commands/companion/update.ts +75 -42
- package/src/cli/commands/doctor.ts +176 -3
- package/src/cli/commands/plugin/install.ts +9 -4
- package/src/cli/commands/plugin/plugin.ts +3 -2
- package/src/cli/commands/report/adapter.ts +11 -2
- package/src/cli/commands/report/contract.ts +21 -0
- package/src/cli/commands/report/highlight.ts +34 -4
- package/src/cli/commands/report/markdown.ts +110 -0
- package/src/cli/commands/report/palette.ts +118 -0
- package/src/cli/commands/report/renderer.ts +456 -153
- package/src/cli/commands/report/types.ts +16 -0
- package/src/cli/commands/setup.ts +7 -15
- package/src/cli/commands/studio/client-asset.ts +16 -0
- package/src/cli/commands/studio/openspec-cli.ts +10 -58
- package/src/cli/commands/studio/routes.ts +2 -0
- package/src/cli/commands/studio/selection.ts +19 -1
- package/src/cli/commands/studio/server.ts +42 -1
- package/src/cli/commands/studio/views/client.ts +6 -616
- package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
- package/src/cli/commands/studio/views/document.tsx +137 -69
- package/src/cli/commands/studio/views/markdown.tsx +250 -0
- package/src/cli/commands/studio/views/palette.ts +134 -0
- package/src/cli/commands/studio/views/reports/index.tsx +2 -7
- package/src/cli/commands/studio/views/skills/index.tsx +43 -62
- package/src/cli/commands/studio/views/specs/index.tsx +7 -15
- package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
- package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
- package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
- package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
- package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
- package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
- package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
- package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
- package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
- package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
- package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
- package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
- package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
- package/src/cli/commands/studio/views/styles.ts +217 -206
- package/src/cli/commands/studio/views/vault/state.ts +3 -5
- package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
- package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
- package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
- package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
- package/src/cli/commands/wrap.ts +5 -13
- package/src/cli/main.ts +4 -1
- package/src/cli/parse-flags.ts +25 -7
- package/src/cli/setup-selector.tsx +38 -162
- package/src/cli/usage.ts +4 -0
- package/src/hooks/companion-instructions.ts +190 -0
- package/src/hooks/hook-io.ts +63 -0
- package/src/hooks/session-banner.ts +21 -29
- package/src/hooks/session-guidance.ts +9 -17
- package/src/hooks/session-prepare.ts +115 -28
- package/src/hooks/validate-artifact-path.ts +82 -24
- package/src/index.ts +4 -0
- package/src/lib/change-layout.ts +154 -0
- package/src/lib/components/wizard-header.tsx +3 -1
- package/src/lib/context-mode-package.ts +24 -11
- package/src/lib/delivery/archive-change.ts +154 -0
- package/src/lib/delivery/branches.ts +108 -0
- package/src/lib/delivery/environment.ts +29 -0
- package/src/lib/delivery/forge.ts +279 -0
- package/src/lib/delivery/overlap.ts +202 -0
- package/src/lib/delivery/record-delivery.ts +196 -0
- package/src/lib/delivery/record.ts +343 -0
- package/src/lib/delivery/refresh.ts +174 -0
- package/src/lib/delivery/repository-access.ts +97 -0
- package/src/lib/delivery/stages.ts +111 -0
- package/src/lib/delivery/token.ts +58 -0
- package/src/lib/frontmatter.ts +18 -0
- package/src/lib/opencode-plugin-package.ts +5 -5
- package/src/lib/openspec-cli.ts +116 -0
- package/src/lib/openspec-scopes.ts +86 -0
- package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
- package/src/lib/orchestrator/adapters/claude.ts +48 -40
- package/src/lib/orchestrator/adapters/opencode.ts +2 -0
- package/src/lib/orchestrator/editor.ts +9 -9
- package/src/lib/orchestrator/openspec-store.ts +14 -54
- package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
- package/src/lib/orchestrator/projection-claude-links.ts +159 -0
- package/src/lib/orchestrator/projection-claude-skills.ts +19 -137
- package/src/lib/orchestrator/projection-entries.ts +45 -14
- package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
- package/src/lib/orchestrator/projection-types.ts +3 -0
- package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
- package/src/lib/orchestrator/session-prepare.ts +45 -5
- package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
- package/src/lib/orchestrator/types.ts +2 -2
- package/src/lib/orchestrator/wrap-pass.ts +22 -1
- package/src/lib/package-paths.ts +8 -0
- package/src/opencode/companion-hooks.ts +64 -5
- package/src/opencode/companion-policy.ts +37 -18
- package/src/opencode/projected-guidance.ts +4 -1
- package/src/opencode/session-prepare.ts +2 -1
- package/src/opencode/tui.tsx +4 -2
- package/src/runtime/artifact-refusal.ts +63 -0
- package/src/runtime/claude-delivery-plan.ts +89 -7
- package/src/runtime/commit-attribution-guard.ts +181 -0
- package/src/runtime/companion-guidance.ts +15 -3
- package/src/runtime/companion-instructions.ts +340 -0
- package/src/runtime/error-message.ts +4 -0
- package/src/runtime/index.ts +5 -0
- package/src/runtime/managed-block.ts +117 -0
- package/src/runtime/markdown-scan.ts +266 -0
- package/src/runtime/policy.ts +15 -1
- package/src/runtime/projected-flag.ts +7 -0
- package/src/runtime/projected-guidance.ts +1 -0
- package/src/runtime/runtime-document-manifest.ts +62 -11
- package/src/runtime/secret-guard.ts +148 -0
- package/src/runtime/secret-patterns.ts +164 -0
- package/src/runtime/secret-scan.ts +90 -0
- package/src/runtime/session-kind.ts +13 -0
- package/src/runtime/shell-heredoc.ts +81 -0
- package/src/runtime/wrap-state.ts +36 -2
- package/src/runtime/write-atomic.ts +64 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
- package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
- package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
- package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
- package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
- package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
- package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
- package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
- package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
- package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
- package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
- package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
- package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
- package/src/templates/mate-skills/{claude/mate-artifact-publish → agents/mate-publish-artifact}/SKILL.md +91 -23
- package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
- package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
- package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
- package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
- package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
- package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
- package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
- package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
- package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
- package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
- package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
- package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
- package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
- package/src/templates/mate-skills/{agents/mate-artifact-publish → claude/mate-publish-artifact}/SKILL.md +91 -23
- package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
- package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
- package/src/templates/mate-skills/openspec-references.ts +140 -0
- package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
- package/src/tools/setup/capabilities/context-mode.ts +25 -5
- package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
- package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
- package/src/tools/setup/capabilities/graphify.ts +12 -0
- package/src/tools/setup/capabilities/openspec.ts +151 -270
- package/src/tools/setup/capabilities/tokensave.ts +36 -3
- package/src/tools/setup/context-services.ts +9 -17
- package/src/tools/setup/dynamic-plugins/install.ts +24 -28
- package/src/tools/setup/engine.ts +97 -21
- package/src/tools/setup/managed-output.ts +51 -0
- package/src/tools/setup/mate.ts +86 -11
- package/src/tools/setup/plugin.ts +35 -0
- package/src/tools/setup/plugins/guidance.ts +7 -4
- package/src/tools/setup/providers/agent-file-sections.ts +4 -2
- package/src/tools/setup/providers/claude-attribution.ts +65 -0
- package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
- package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
- package/src/tools/setup/providers/claude.ts +284 -8
- package/src/tools/setup/providers/opencode.ts +180 -1
- package/src/tools/setup/utils.ts +14 -4
- package/src/tools/setup.ts +126 -10
- package/wrappers/bin/graphify +52 -4
- package/wrappers/bin/openspec +33 -9
- package/wrappers/lib/graphify-gate.mjs +8 -0
- package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
- package/src/tools/setup/runtime-documents.ts +0 -291
|
@@ -10,13 +10,22 @@ import {
|
|
|
10
10
|
type StudioView,
|
|
11
11
|
switchView,
|
|
12
12
|
toHref,
|
|
13
|
+
withMode,
|
|
13
14
|
} from "../selection";
|
|
14
|
-
import {
|
|
15
|
-
|
|
15
|
+
import {
|
|
16
|
+
REPORTS_ROUTE,
|
|
17
|
+
VAULT_CHANGES_ROUTE,
|
|
18
|
+
VAULT_DIR_ROUTE,
|
|
19
|
+
VAULT_FILTER_ROUTE,
|
|
20
|
+
VAULT_PANE_ROUTE,
|
|
21
|
+
} from "../routes";
|
|
22
|
+
import { STUDIO_CLIENT_ROUTE } from "../client-asset";
|
|
23
|
+
import { STUDIO_PREPAINT_SCRIPT } from "./client";
|
|
16
24
|
import { CompanionPicker } from "./companion-picker";
|
|
17
25
|
import { CompanionSelector } from "./companion-selector";
|
|
18
26
|
import { Dashboard } from "./dashboard/index";
|
|
19
27
|
import { CompanionError } from "./error";
|
|
28
|
+
import { MarkdownView } from "./markdown";
|
|
20
29
|
import { formatCollectedAt, type StudioPage, type StudioVaultPage } from "./model";
|
|
21
30
|
import type { VaultTreeNode } from "../vault";
|
|
22
31
|
import { Reports } from "./reports/index";
|
|
@@ -34,35 +43,18 @@ import {
|
|
|
34
43
|
TerminalSidebar,
|
|
35
44
|
} from "./terminal";
|
|
36
45
|
import { TERMINAL_SCRIPTS, TERMINAL_STYLESHEET } from "../terminal-assets";
|
|
46
|
+
import { BRAND_SVGS } from "../../../../brand/svg.generated";
|
|
37
47
|
|
|
38
48
|
const STUDIO_TITLE = "Mate Studio";
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
},
|
|
49
|
-
workflow: {
|
|
50
|
-
eyebrow: "Companion lookup",
|
|
51
|
-
title: "Workflow",
|
|
52
|
-
},
|
|
53
|
-
skills: {
|
|
54
|
-
eyebrow: "Agent guidance",
|
|
55
|
-
title: "Skills",
|
|
56
|
-
},
|
|
57
|
-
vault: {
|
|
58
|
-
eyebrow: "Companion files",
|
|
59
|
-
title: "Vault",
|
|
60
|
-
},
|
|
61
|
-
reports: {
|
|
62
|
-
eyebrow: "Agent output",
|
|
63
|
-
title: "Reports",
|
|
64
|
-
},
|
|
65
|
-
} as const;
|
|
50
|
+
/** Base64 rather than URI-encoded, so the SVG namespace URL never reads as a page reference. */
|
|
51
|
+
function svgDataUri(svg: string): string {
|
|
52
|
+
return `data:image/svg+xml;base64,${Buffer.from(svg, "utf8").toString("base64")}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const FAVICON_URI = svgDataUri(BRAND_SVGS.favicon);
|
|
56
|
+
const SYMBOL_COLOR_URI = svgDataUri(BRAND_SVGS.symbolColor);
|
|
57
|
+
const SYMBOL_ON_DARK_URI = svgDataUri(BRAND_SVGS.symbolOnDark);
|
|
66
58
|
|
|
67
59
|
/**
|
|
68
60
|
* `<style>` and `<script>` are HTML raw-text elements: a character reference
|
|
@@ -85,11 +77,12 @@ export function renderStudioDocument(page: StudioPage): string {
|
|
|
85
77
|
<meta charset="utf-8">
|
|
86
78
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
87
79
|
<title>${STUDIO_TITLE}</title>
|
|
80
|
+
<link rel="icon" type="image/svg+xml" href="${FAVICON_URI}">
|
|
88
81
|
<style>${STUDIO_STYLES}</style>
|
|
89
82
|
<script>${STUDIO_PREPAINT_SCRIPT}</script>${terminalHead}
|
|
90
83
|
</head>
|
|
91
84
|
<body>${String(<StudioShell page={page} />)}
|
|
92
|
-
<script
|
|
85
|
+
<script src="${STUDIO_CLIENT_ROUTE}"></script>${terminalBody}
|
|
93
86
|
</body>
|
|
94
87
|
</html>
|
|
95
88
|
`;
|
|
@@ -174,10 +167,11 @@ function Sidebar({ page }: { page: StudioPage }) {
|
|
|
174
167
|
return (
|
|
175
168
|
<aside className="rail studio-sidebar" id="studio-rail">
|
|
176
169
|
<div className="sidebar-brand">
|
|
177
|
-
|
|
170
|
+
{/* Both versions ship; the stylesheet shows the one matching the appearance. */}
|
|
171
|
+
<img className="sidebar-brand-mark brand-symbol-color" src={SYMBOL_COLOR_URI} alt="" />
|
|
172
|
+
<img className="sidebar-brand-mark brand-symbol-on-dark" src={SYMBOL_ON_DARK_URI} alt="" />
|
|
178
173
|
<div className="sidebar-brand-copy">
|
|
179
174
|
<strong>{STUDIO_TITLE}</strong>
|
|
180
|
-
<span>Developer lookup</span>
|
|
181
175
|
</div>
|
|
182
176
|
</div>
|
|
183
177
|
<CompanionSelector inventory={page.inventory} selection={page.selection} />
|
|
@@ -191,13 +185,10 @@ function Sidebar({ page }: { page: StudioPage }) {
|
|
|
191
185
|
function PageHeader({ page }: { page: StudioPage }) {
|
|
192
186
|
if (!page.companion) return null;
|
|
193
187
|
|
|
194
|
-
const details = VIEW_DETAILS[page.selection.view];
|
|
195
|
-
|
|
196
188
|
return (
|
|
197
189
|
<header className="page-header">
|
|
198
190
|
<div className="page-heading">
|
|
199
|
-
<
|
|
200
|
-
<h1>{details.title}</h1>
|
|
191
|
+
<h1>{VIEW_TITLES[page.selection.view]}</h1>
|
|
201
192
|
</div>
|
|
202
193
|
<div className="page-context">
|
|
203
194
|
<span className="page-context-label">Companion Repository</span>
|
|
@@ -222,6 +213,9 @@ const VIEW_NAV: readonly (readonly [StudioView, string])[] = [
|
|
|
222
213
|
["reports", "Reports"],
|
|
223
214
|
];
|
|
224
215
|
|
|
216
|
+
/** A view's page title is its navigation label. */
|
|
217
|
+
const VIEW_TITLES = Object.fromEntries(VIEW_NAV) as Record<StudioView, string>;
|
|
218
|
+
|
|
225
219
|
/**
|
|
226
220
|
* One GET form per view, so switching a view is a navigation to the URL naming
|
|
227
221
|
* it and the browser's history moves between rendered states.
|
|
@@ -306,6 +300,18 @@ export function renderVaultView(page: StudioPage): string {
|
|
|
306
300
|
);
|
|
307
301
|
}
|
|
308
302
|
|
|
303
|
+
/** The open file's breadcrumb and pane, for the page to swap in without a reload. */
|
|
304
|
+
export function renderVaultPane(page: StudioPage): string {
|
|
305
|
+
const { selection, vault } = page;
|
|
306
|
+
if (!vault || selection.view !== "vault") return "";
|
|
307
|
+
return String(
|
|
308
|
+
<>
|
|
309
|
+
<VaultBreadcrumb page={page} selection={selection} />
|
|
310
|
+
<VaultPane page={page} selection={selection} vault={vault} />
|
|
311
|
+
</>,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
309
315
|
/** A folder's entries, for a folder the page did not expand; `null` when the listed tree lacks it. */
|
|
310
316
|
export function renderVaultChildren(page: StudioPage): string | null {
|
|
311
317
|
const { selection, vault } = page;
|
|
@@ -437,11 +443,12 @@ function Vault({ page, selection }: { page: StudioPage; selection: StudioVaultSe
|
|
|
437
443
|
<p className="empty">No vault state collected.</p>
|
|
438
444
|
</section>
|
|
439
445
|
);
|
|
440
|
-
const showListing = !vault.open && !vault.refusal;
|
|
441
446
|
return (
|
|
442
447
|
<section
|
|
443
448
|
className="vault-layout"
|
|
444
449
|
data-vault-layout
|
|
450
|
+
data-vault-pane-route={VAULT_PANE_ROUTE}
|
|
451
|
+
data-vault-scope={selection.companionDigest ?? ""}
|
|
445
452
|
data-vault-view={vault.tree === null ? toHref(selection, "/api/vault/view") : undefined}
|
|
446
453
|
data-vault-refresh-url={toHref(refresh(selection), "/api/vault/view")}
|
|
447
454
|
data-vault-filter-url={toHref(plain(selection), VAULT_FILTER_ROUTE)}
|
|
@@ -467,7 +474,7 @@ function Vault({ page, selection }: { page: StudioPage; selection: StudioVaultSe
|
|
|
467
474
|
<div id="vault-filter-results" data-vault-filter-results hidden />
|
|
468
475
|
<VaultTreeSlot vault={vault} selection={selection} />
|
|
469
476
|
</aside>
|
|
470
|
-
<div className="vault-main">
|
|
477
|
+
<div className="vault-main" data-vault-main>
|
|
471
478
|
<div className="vault-bar">
|
|
472
479
|
<button
|
|
473
480
|
type="button"
|
|
@@ -481,8 +488,7 @@ function Vault({ page, selection }: { page: StudioPage; selection: StudioVaultSe
|
|
|
481
488
|
</button>
|
|
482
489
|
<VaultBreadcrumb page={page} selection={selection} />
|
|
483
490
|
</div>
|
|
484
|
-
{
|
|
485
|
-
{showListing ? null : <VaultEditor page={page} selection={selection} vault={vault} />}
|
|
491
|
+
<VaultPane page={page} selection={selection} vault={vault} />
|
|
486
492
|
</div>
|
|
487
493
|
</section>
|
|
488
494
|
);
|
|
@@ -647,7 +653,7 @@ function VaultBreadcrumb({
|
|
|
647
653
|
? (segments(page.companion.path).at(-1) ?? "companion")
|
|
648
654
|
: "companion";
|
|
649
655
|
return (
|
|
650
|
-
<nav className="vault-breadcrumb" aria-label="Breadcrumb">
|
|
656
|
+
<nav className="vault-breadcrumb" aria-label="Breadcrumb" data-vault-crumb>
|
|
651
657
|
<ol>
|
|
652
658
|
<li>
|
|
653
659
|
{parts.length === 0 ? (
|
|
@@ -768,7 +774,7 @@ function VaultListingEntry({
|
|
|
768
774
|
);
|
|
769
775
|
}
|
|
770
776
|
return (
|
|
771
|
-
<form method="get" action="/" className="vault-row" data-studio-navigation>
|
|
777
|
+
<form method="get" action="/" className="vault-row" data-studio-navigation data-vault-open>
|
|
772
778
|
<SelectionFields selection={openFile(selection, node.path)} />
|
|
773
779
|
<button type="submit">
|
|
774
780
|
<FileIcon />
|
|
@@ -778,7 +784,29 @@ function VaultListingEntry({
|
|
|
778
784
|
);
|
|
779
785
|
}
|
|
780
786
|
|
|
781
|
-
|
|
787
|
+
/** What the in-place navigation swaps: the listing, or the open file read or edited. */
|
|
788
|
+
function VaultPane({
|
|
789
|
+
page,
|
|
790
|
+
selection,
|
|
791
|
+
vault,
|
|
792
|
+
}: {
|
|
793
|
+
page: StudioPage;
|
|
794
|
+
selection: StudioVaultSelection;
|
|
795
|
+
vault: StudioVaultPage;
|
|
796
|
+
}) {
|
|
797
|
+
const showListing = !vault.open && !vault.refusal && !selection.openPath;
|
|
798
|
+
return (
|
|
799
|
+
<div data-vault-pane>
|
|
800
|
+
{showListing ? (
|
|
801
|
+
<VaultListingSlot selection={selection} vault={vault} />
|
|
802
|
+
) : (
|
|
803
|
+
<VaultFile page={page} selection={selection} vault={vault} />
|
|
804
|
+
)}
|
|
805
|
+
</div>
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function VaultFile({
|
|
782
810
|
page,
|
|
783
811
|
selection,
|
|
784
812
|
vault,
|
|
@@ -797,46 +825,86 @@ function VaultEditor({
|
|
|
797
825
|
</div>
|
|
798
826
|
);
|
|
799
827
|
}
|
|
828
|
+
const { open } = vault;
|
|
829
|
+
// Without the write path a file is only ever read, whatever the address names.
|
|
830
|
+
const editing = page.writable && selection.mode === "edit";
|
|
831
|
+
const readAddress = withMode(selection, "read");
|
|
800
832
|
return (
|
|
801
833
|
<div
|
|
802
834
|
className="panel vault-editor-panel"
|
|
803
835
|
data-vault-root
|
|
836
|
+
data-vault-mode={editing ? "edit" : "read"}
|
|
804
837
|
data-vault-writable={page.writable ? "true" : "false"}
|
|
838
|
+
data-vault-path={open.path}
|
|
839
|
+
data-vault-events={toHref(openFile(selection, open.path), "/api/vault/events")}
|
|
840
|
+
data-vault-pane-url={toHref(readAddress, VAULT_PANE_ROUTE)}
|
|
841
|
+
data-vault-read-href={toHref(readAddress)}
|
|
805
842
|
>
|
|
806
843
|
<div className="section-header">
|
|
807
844
|
<div>
|
|
808
|
-
<h3>{segments(
|
|
845
|
+
<h3>{segments(open.path).at(-1) ?? open.path}</h3>
|
|
809
846
|
<p className="section-note">
|
|
810
|
-
{page.writable
|
|
811
|
-
? "
|
|
812
|
-
:
|
|
847
|
+
{!page.writable
|
|
848
|
+
? "Read-only mode. Start Studio with --writable to edit."
|
|
849
|
+
: editing
|
|
850
|
+
? "Changes stay in the editor until you save."
|
|
851
|
+
: "Rendered view."}
|
|
813
852
|
</p>
|
|
814
853
|
</div>
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
854
|
+
<div className="vault-actions">
|
|
855
|
+
{editing ? (
|
|
856
|
+
<>
|
|
857
|
+
<button type="button" id="vault-save">
|
|
858
|
+
Save
|
|
859
|
+
</button>
|
|
860
|
+
<a
|
|
861
|
+
className="vault-mode-link"
|
|
862
|
+
id="vault-mode-read"
|
|
863
|
+
href={toHref(readAddress)}
|
|
864
|
+
data-vault-mode-link
|
|
865
|
+
>
|
|
866
|
+
Read
|
|
867
|
+
</a>
|
|
868
|
+
</>
|
|
869
|
+
) : page.writable ? (
|
|
870
|
+
<a
|
|
871
|
+
className="vault-mode-link"
|
|
872
|
+
id="vault-mode-edit"
|
|
873
|
+
href={toHref(withMode(selection, "edit"))}
|
|
874
|
+
data-vault-mode-link
|
|
875
|
+
>
|
|
876
|
+
Edit
|
|
877
|
+
</a>
|
|
878
|
+
) : null}
|
|
879
|
+
</div>
|
|
820
880
|
</div>
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
881
|
+
{editing ? (
|
|
882
|
+
<textarea
|
|
883
|
+
id="vault-editor"
|
|
884
|
+
data-vault-editor
|
|
885
|
+
data-vault-path={open.path}
|
|
886
|
+
data-vault-token={open.token}
|
|
887
|
+
spellCheck={false}
|
|
888
|
+
>
|
|
889
|
+
{open.content}
|
|
890
|
+
</textarea>
|
|
891
|
+
) : (
|
|
892
|
+
<MarkdownView
|
|
893
|
+
source={open.content}
|
|
894
|
+
path={open.path}
|
|
895
|
+
hrefFor={(target) => toHref(openFile(selection, target))}
|
|
896
|
+
/>
|
|
897
|
+
)}
|
|
832
898
|
<p id="vault-status" className="note" aria-live="polite" />
|
|
833
|
-
|
|
834
|
-
<
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
899
|
+
{editing ? (
|
|
900
|
+
<div id="vault-incoming" className="vault-incoming" hidden>
|
|
901
|
+
<strong id="vault-incoming-title">Incoming version</strong>
|
|
902
|
+
<pre id="vault-incoming-content" />
|
|
903
|
+
<button type="button" id="vault-recover">
|
|
904
|
+
Put incoming content back
|
|
905
|
+
</button>
|
|
906
|
+
</div>
|
|
907
|
+
) : null}
|
|
840
908
|
</div>
|
|
841
909
|
);
|
|
842
910
|
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/** @jsxImportSource hono/jsx */
|
|
2
|
+
|
|
3
|
+
import { type Token, type Tokens, Lexer } from "marked";
|
|
4
|
+
|
|
5
|
+
import { highlightNodes } from "../../report/highlight";
|
|
6
|
+
|
|
7
|
+
type Child = unknown;
|
|
8
|
+
|
|
9
|
+
export interface MarkdownViewProps {
|
|
10
|
+
source: string;
|
|
11
|
+
/** The open file's companion-relative path; relative links resolve against its folder. */
|
|
12
|
+
path: string;
|
|
13
|
+
/** The vault address of a companion-relative markdown path. */
|
|
14
|
+
hrefFor: (path: string) => string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const MARKDOWN_TARGET = /\.(md|markdown)$/i;
|
|
18
|
+
const SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A relative link as a companion-relative markdown path, or `null` when it is anything
|
|
22
|
+
* else. Textual only: nothing is looked up on disk, so a missing note is the vault's to refuse.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveNoteLink(target: string, openPath: string): string | null {
|
|
25
|
+
if (!target || SCHEME.test(target) || target.startsWith("//") || target.startsWith("#")) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
let pathname = target.split(/[?#]/, 1)[0] ?? "";
|
|
29
|
+
try {
|
|
30
|
+
pathname = decodeURIComponent(pathname);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (pathname.includes("\\") || !MARKDOWN_TARGET.test(pathname)) return null;
|
|
35
|
+
const folder = pathname.startsWith("/") ? [] : openPath.split("/").filter(Boolean).slice(0, -1);
|
|
36
|
+
for (const part of pathname.split("/")) {
|
|
37
|
+
if (part === "" || part === ".") continue;
|
|
38
|
+
if (part === "..") {
|
|
39
|
+
if (folder.length === 0) return null;
|
|
40
|
+
folder.pop();
|
|
41
|
+
} else {
|
|
42
|
+
folder.push(part);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return folder.length > 0 ? folder.join("/") : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isWebAddress(target: string): boolean {
|
|
49
|
+
return /^https?:\/\//i.test(target);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function Highlighted({ code, language }: { code: string; language: string | undefined }) {
|
|
53
|
+
const render = (node: ReturnType<typeof highlightNodes>[number], key: number): Child =>
|
|
54
|
+
node.type === "text" ? (
|
|
55
|
+
node.value
|
|
56
|
+
) : (
|
|
57
|
+
<span key={key} className={node.classNames.join(" ")}>
|
|
58
|
+
{node.children.map(render)}
|
|
59
|
+
</span>
|
|
60
|
+
);
|
|
61
|
+
return <>{highlightNodes(code, language).map(render)}</>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Plain text of inline tokens, for an image's alternative text. */
|
|
65
|
+
function plainText(tokens: Token[] | undefined, fallback: string): string {
|
|
66
|
+
if (!tokens) return fallback;
|
|
67
|
+
return tokens
|
|
68
|
+
.map((token) => ("tokens" in token && token.tokens ? plainText(token.tokens, "") : token.raw))
|
|
69
|
+
.join("");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class Mapper {
|
|
73
|
+
constructor(private readonly props: MarkdownViewProps) {}
|
|
74
|
+
|
|
75
|
+
blocks(tokens: Token[] | undefined): Child[] {
|
|
76
|
+
return (tokens ?? []).map((token, index) => this.block(token, index));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
inline(tokens: Token[] | undefined, fallback = ""): Child[] {
|
|
80
|
+
if (!tokens) return [fallback];
|
|
81
|
+
return tokens.map((token, index) => this.span(token, index));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private block(token: Token, key: number): Child {
|
|
85
|
+
switch (token.type) {
|
|
86
|
+
case "space":
|
|
87
|
+
case "def":
|
|
88
|
+
return null;
|
|
89
|
+
case "heading": {
|
|
90
|
+
const heading = token as Tokens.Heading;
|
|
91
|
+
const Tag = `h${Math.min(Math.max(heading.depth, 1), 6)}` as "h1";
|
|
92
|
+
return <Tag key={key}>{this.inline(heading.tokens, heading.text)}</Tag>;
|
|
93
|
+
}
|
|
94
|
+
case "paragraph": {
|
|
95
|
+
const paragraph = token as Tokens.Paragraph;
|
|
96
|
+
return <p key={key}>{this.inline(paragraph.tokens, paragraph.text)}</p>;
|
|
97
|
+
}
|
|
98
|
+
case "text": {
|
|
99
|
+
const text = token as Tokens.Text;
|
|
100
|
+
return <p key={key}>{this.inline(text.tokens, text.text)}</p>;
|
|
101
|
+
}
|
|
102
|
+
case "code": {
|
|
103
|
+
const code = token as Tokens.Code;
|
|
104
|
+
const language = code.lang?.trim().split(/\s+/, 1)[0] || undefined;
|
|
105
|
+
return (
|
|
106
|
+
<pre key={key} className="md-fence">
|
|
107
|
+
<code>
|
|
108
|
+
<Highlighted code={code.text} language={language} />
|
|
109
|
+
</code>
|
|
110
|
+
</pre>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
case "blockquote":
|
|
114
|
+
return (
|
|
115
|
+
<blockquote key={key}>{this.blocks((token as Tokens.Blockquote).tokens)}</blockquote>
|
|
116
|
+
);
|
|
117
|
+
case "hr":
|
|
118
|
+
return <hr key={key} />;
|
|
119
|
+
case "list":
|
|
120
|
+
return this.list(token as Tokens.List, key);
|
|
121
|
+
case "table":
|
|
122
|
+
return this.table(token as Tokens.Table, key);
|
|
123
|
+
case "html":
|
|
124
|
+
// Raw markup is shown as the text it is, never inserted.
|
|
125
|
+
return (
|
|
126
|
+
<p key={key} className="md-raw">
|
|
127
|
+
{(token as Tokens.HTML).raw.trim()}
|
|
128
|
+
</p>
|
|
129
|
+
);
|
|
130
|
+
default:
|
|
131
|
+
return <p key={key}>{token.raw}</p>;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private list(list: Tokens.List, key: number): Child {
|
|
136
|
+
const Tag = list.ordered ? "ol" : "ul";
|
|
137
|
+
const start =
|
|
138
|
+
list.ordered && typeof list.start === "number" && list.start !== 1 ? list.start : undefined;
|
|
139
|
+
return (
|
|
140
|
+
<Tag key={key} start={start}>
|
|
141
|
+
{list.items.map((item, index) => {
|
|
142
|
+
const body = item.tokens.filter((token) => token.type !== "checkbox");
|
|
143
|
+
return (
|
|
144
|
+
<li key={index} className={item.task ? "md-task" : undefined}>
|
|
145
|
+
{item.task ? (
|
|
146
|
+
<input type="checkbox" checked={!!item.checked} disabled aria-label="Task" />
|
|
147
|
+
) : null}
|
|
148
|
+
{item.loose ? this.blocks(body) : this.tight(body)}
|
|
149
|
+
</li>
|
|
150
|
+
);
|
|
151
|
+
})}
|
|
152
|
+
</Tag>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** A tight item's text sits in the item, not in a paragraph of its own. */
|
|
157
|
+
private tight(tokens: Token[]): Child[] {
|
|
158
|
+
return tokens.map((token, index) =>
|
|
159
|
+
token.type === "text"
|
|
160
|
+
? this.inline((token as Tokens.Text).tokens, (token as Tokens.Text).text)
|
|
161
|
+
: this.block(token, index),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private table(table: Tokens.Table, key: number): Child {
|
|
166
|
+
const align = (cell: Tokens.TableCell) => (cell.align ? { textAlign: cell.align } : undefined);
|
|
167
|
+
return (
|
|
168
|
+
<div key={key} className="md-table">
|
|
169
|
+
<table>
|
|
170
|
+
<thead>
|
|
171
|
+
<tr>
|
|
172
|
+
{table.header.map((cell, index) => (
|
|
173
|
+
<th key={index} style={align(cell)}>
|
|
174
|
+
{this.inline(cell.tokens, cell.text)}
|
|
175
|
+
</th>
|
|
176
|
+
))}
|
|
177
|
+
</tr>
|
|
178
|
+
</thead>
|
|
179
|
+
<tbody>
|
|
180
|
+
{table.rows.map((row, rowIndex) => (
|
|
181
|
+
<tr key={rowIndex}>
|
|
182
|
+
{row.map((cell, index) => (
|
|
183
|
+
<td key={index} style={align(cell)}>
|
|
184
|
+
{this.inline(cell.tokens, cell.text)}
|
|
185
|
+
</td>
|
|
186
|
+
))}
|
|
187
|
+
</tr>
|
|
188
|
+
))}
|
|
189
|
+
</tbody>
|
|
190
|
+
</table>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private span(token: Token, key: number): Child {
|
|
196
|
+
switch (token.type) {
|
|
197
|
+
case "text": {
|
|
198
|
+
const text = token as Tokens.Text;
|
|
199
|
+
return text.tokens ? <span key={key}>{this.inline(text.tokens)}</span> : text.text;
|
|
200
|
+
}
|
|
201
|
+
case "escape":
|
|
202
|
+
return (token as Tokens.Escape).text;
|
|
203
|
+
case "strong":
|
|
204
|
+
return <strong key={key}>{this.inline((token as Tokens.Strong).tokens)}</strong>;
|
|
205
|
+
case "em":
|
|
206
|
+
return <em key={key}>{this.inline((token as Tokens.Em).tokens)}</em>;
|
|
207
|
+
case "del":
|
|
208
|
+
return <del key={key}>{this.inline((token as Tokens.Del).tokens)}</del>;
|
|
209
|
+
case "codespan":
|
|
210
|
+
return <code key={key}>{(token as Tokens.Codespan).text}</code>;
|
|
211
|
+
case "br":
|
|
212
|
+
return <br key={key} />;
|
|
213
|
+
case "link":
|
|
214
|
+
return this.link(token as Tokens.Link, key);
|
|
215
|
+
case "image": {
|
|
216
|
+
// Not requested or shown: only what the author said the image is.
|
|
217
|
+
const image = token as Tokens.Image;
|
|
218
|
+
return image.text || plainText(image.tokens, "");
|
|
219
|
+
}
|
|
220
|
+
case "html":
|
|
221
|
+
return (token as Tokens.HTML).raw;
|
|
222
|
+
default:
|
|
223
|
+
return token.raw;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private link(link: Tokens.Link, key: number): Child {
|
|
228
|
+
const label = this.inline(link.tokens, link.text);
|
|
229
|
+
if (isWebAddress(link.href)) {
|
|
230
|
+
return (
|
|
231
|
+
<a key={key} href={link.href} target="_blank" rel="noopener noreferrer">
|
|
232
|
+
{label}
|
|
233
|
+
</a>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const note = resolveNoteLink(link.href, this.props.path);
|
|
237
|
+
if (note === null) return <span key={key}>{label}</span>;
|
|
238
|
+
return (
|
|
239
|
+
<a key={key} href={this.props.hrefFor(note)} data-vault-entry={note}>
|
|
240
|
+
{label}
|
|
241
|
+
</a>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Rendered from lexer tokens to elements, so no part of the file is ever inserted as markup. */
|
|
247
|
+
export function MarkdownView(props: MarkdownViewProps) {
|
|
248
|
+
const tokens = new Lexer({ gfm: true }).lex(props.source);
|
|
249
|
+
return <div className="markdown-body">{new Mapper(props).blocks(tokens)}</div>;
|
|
250
|
+
}
|