@uniqbit/mate-core 0.15.5 → 0.16.0-canary.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/claude-plugin/.claude-plugin/plugin.json +2 -2
- package/claude-plugin/hooks/hooks.json +3 -6
- package/claude-plugin/hooks/session-guidance.mjs +8 -0
- package/claude-plugin/hooks/ts-loader.mjs +19 -0
- package/package.json +7 -5
- package/src/cli/commands/artifact/artifact.ts +19 -3
- package/src/cli/commands/artifact/finish/command.ts +183 -57
- package/src/cli/commands/artifact/finish/engine.ts +80 -91
- package/src/cli/commands/artifact/finish/finisher.ts +26 -33
- package/src/cli/commands/artifact/finish/git.ts +34 -23
- package/src/cli/commands/artifact/finish/index.ts +9 -3
- package/src/cli/commands/artifact/finish/openspec.ts +225 -97
- package/src/cli/commands/artifact/pending/command.ts +175 -0
- package/src/cli/commands/artifact/pending/discovery.ts +249 -0
- package/src/cli/commands/artifact/pending/index.ts +17 -0
- package/src/cli/commands/cap/index-cmd.ts +9 -1
- package/src/cli/commands/cap/index.ts +2 -6
- package/src/cli/commands/cap/tokensave.ts +4 -4
- package/src/cli/commands/companion/companion.ts +5 -1
- package/src/cli/commands/companion/link.ts +2 -2
- package/src/cli/commands/companion/sync.ts +92 -0
- package/src/cli/commands/doctor.ts +0 -3
- package/src/cli/commands/launch/shared.ts +23 -5
- package/src/cli/commands/report/collector.ts +72 -78
- package/src/cli/commands/report/contract.ts +40 -1
- package/src/cli/commands/report/highlight.ts +27 -0
- package/src/cli/commands/report/index.ts +11 -18
- package/src/cli/commands/report/renderer.ts +199 -2
- package/src/cli/commands/report/types.ts +26 -1
- package/src/cli/commands/shared/companion-selection.ts +107 -10
- package/src/cli/commands/studio/areas.ts +68 -0
- package/src/cli/commands/studio/index.ts +69 -0
- package/src/cli/commands/studio/inventory.ts +55 -0
- package/src/cli/commands/studio/mate-inventory.ts +43 -0
- package/src/cli/commands/studio/openspec-cli.ts +198 -0
- package/src/cli/commands/studio/payload.ts +184 -0
- package/src/cli/commands/studio/routes.ts +2 -0
- package/src/cli/commands/studio/selection.ts +61 -0
- package/src/cli/commands/studio/server.ts +201 -0
- package/src/cli/commands/studio/snapshot.ts +63 -0
- package/src/cli/commands/studio/topology.ts +199 -0
- package/src/cli/commands/studio/views/client.ts +197 -0
- package/src/cli/commands/studio/views/companion-picker.tsx +91 -0
- package/src/cli/commands/studio/views/companion-selector.tsx +90 -0
- package/src/cli/commands/studio/views/dashboard/changes.tsx +107 -0
- package/src/cli/commands/studio/views/dashboard/index.tsx +19 -0
- package/src/cli/commands/studio/views/document.tsx +256 -0
- package/src/cli/commands/studio/views/error.tsx +20 -0
- package/src/cli/commands/studio/views/model.ts +34 -0
- package/src/cli/commands/studio/views/pairings.tsx +38 -0
- package/src/cli/commands/studio/views/skills/index.tsx +87 -0
- package/src/cli/commands/studio/views/specs/index.tsx +101 -0
- package/src/cli/commands/studio/views/styles.ts +389 -0
- package/src/cli/commands/studio/views/warnings.tsx +21 -0
- package/src/cli/commands/studio/views/workflow/index.tsx +21 -0
- package/src/cli/commands/studio/views/workflow/steps.ts +329 -0
- package/src/cli/commands/studio/views/workflow/transcript.tsx +190 -0
- package/src/cli/commands/unwrap.ts +70 -0
- package/src/cli/commands/wrap.ts +164 -0
- package/src/cli/main.ts +68 -19
- package/src/cli/parse-flags.ts +36 -11
- package/src/cli/usage.ts +12 -3
- package/src/framework.ts +1 -7
- package/src/hooks/session-banner.ts +64 -11
- package/src/hooks/session-guidance.ts +40 -0
- package/src/hooks/validate-artifact-path.ts +108 -35
- package/src/lib/fs-utils.ts +9 -0
- package/src/lib/install.ts +33 -0
- package/src/lib/orchestrator/adapters/base.ts +14 -125
- package/src/lib/orchestrator/adapters/claude.ts +0 -11
- package/src/lib/orchestrator/adapters/opencode.ts +2 -32
- package/src/lib/orchestrator/companion-git-sync.ts +94 -84
- package/src/lib/orchestrator/config-store.ts +2 -21
- package/src/lib/orchestrator/editor.ts +12 -22
- package/src/lib/orchestrator/framework-context.ts +17 -6
- package/src/lib/orchestrator/global-config-store.ts +1 -1
- package/src/lib/orchestrator/launcher.ts +97 -7
- package/src/lib/orchestrator/opencode-guidance.ts +4 -56
- package/src/lib/orchestrator/projection-claude-entry.ts +198 -0
- package/src/lib/orchestrator/projection-claude-skills.ts +120 -0
- package/src/lib/orchestrator/projection-companion-link.ts +62 -0
- package/src/lib/orchestrator/projection-entries.ts +377 -0
- package/src/lib/orchestrator/projection-record.ts +56 -0
- package/src/lib/orchestrator/projection-runtime-documents.ts +424 -0
- package/src/lib/orchestrator/projection-types.ts +169 -0
- package/src/lib/orchestrator/repo-local-registry.ts +37 -133
- package/src/lib/orchestrator/repo-local-store.ts +96 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -9
- package/src/lib/orchestrator/types.ts +1 -0
- package/src/lib/orchestrator/working-repo-projection.ts +366 -0
- package/src/lib/orchestrator/workspace-inventory.ts +1 -1
- package/src/lib/package-paths.ts +11 -1
- package/src/lib/public-npm.ts +2 -1
- package/src/lib/update-checker.ts +15 -9
- package/src/opencode/companion-hooks.ts +89 -245
- package/src/opencode/companion-policy.ts +35 -10
- package/src/opencode/index.ts +1 -0
- package/src/opencode/projected-guidance.ts +56 -0
- package/src/opencode/tui.tsx +13 -4
- package/src/playbooks/companion-guidance.ts +32 -116
- package/src/plugins.ts +0 -1
- package/src/runtime/companion-git-state.ts +156 -0
- package/src/runtime/companion-git.ts +203 -0
- package/src/runtime/companion-guidance.ts +222 -0
- package/src/runtime/companion-sync.ts +298 -0
- package/src/runtime/env-names.ts +30 -0
- package/src/runtime/env.ts +67 -35
- package/src/runtime/framework.ts +10 -0
- package/src/runtime/freshness.ts +58 -0
- package/src/runtime/index.ts +104 -0
- package/src/runtime/install.ts +30 -0
- package/src/runtime/policy.ts +66 -0
- package/src/runtime/projected-guidance.ts +45 -0
- package/src/runtime/projection.ts +224 -0
- package/src/runtime/repo-local.ts +64 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +56 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +48 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/tasks.md +22 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +31 -90
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/design.md +3 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/explore-brief.md +6 -6
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/tasks.md +3 -0
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +30 -0
- package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +2 -2
- package/src/templates/mate-skills/agents/mate-artifact-publish/SKILL.md +185 -0
- package/src/templates/mate-skills/agents/mate-artifact-publish/references/openspec.md +227 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/SKILL.md +68 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
- package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +14 -0
- package/src/templates/mate-skills/agents/mate-grill-with-docs/SKILL.md +29 -0
- package/src/templates/mate-skills/agents/mate-grilling/SKILL.md +49 -0
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +156 -0
- package/src/templates/{capabilities/openspec-cap/mate-skills → mate-skills}/agents/mate-openspec-backfill/SKILL.md +4 -2
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +139 -0
- package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +503 -0
- package/src/templates/mate-skills/claude/mate-artifact-publish/SKILL.md +185 -0
- package/src/templates/mate-skills/claude/mate-artifact-publish/references/openspec.md +227 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/SKILL.md +68 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
- package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +14 -0
- package/src/templates/mate-skills/claude/mate-grill-with-docs/SKILL.md +29 -0
- package/src/templates/mate-skills/claude/mate-grilling/SKILL.md +49 -0
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +156 -0
- package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +67 -0
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +139 -0
- package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +503 -0
- package/src/templates/report-assets/README.md +32 -0
- package/src/templates/report-assets/mermaid.LICENSE +21 -0
- package/src/templates/report-assets/mermaid.min.js +4376 -0
- package/src/templates/root/TEMPLATE_CLAUDE.md +1 -9
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +1476 -197
- package/src/tools/setup/capabilities/graphify.ts +16 -7
- package/src/tools/setup/capabilities/openspec.ts +63 -56
- package/src/tools/setup/capabilities/tokensave.ts +116 -2
- package/src/tools/setup/engine.ts +34 -6
- package/src/tools/setup/mate.ts +42 -13
- package/src/tools/setup/plugin.ts +9 -0
- package/src/tools/setup/plugins/guidance.ts +11 -1
- package/src/tools/setup/providers/claude-format.ts +49 -4
- package/src/tools/setup/providers/claude-plugin-hooks.ts +117 -0
- package/src/tools/setup/providers/claude.ts +55 -220
- package/src/tools/setup/providers/opencode.ts +41 -14
- package/src/tools/setup/runtime-documents.ts +174 -0
- package/src/tools/setup/surface-target.ts +50 -0
- package/src/tools/setup/working-repo-cleanup.ts +33 -26
- package/src/tools/setup/working-repo-local-state.ts +21 -1
- package/src/tools/setup.ts +25 -3
- package/wrappers/bin/graphify +57 -8
- package/wrappers/bin/openspec +50 -3
- package/claude-plugin/hooks/artifact-finish-nudge.mjs +0 -8
- package/src/cli/commands/cap/headroom.ts +0 -52
- package/src/cli/commands/workspace/list.ts +0 -25
- package/src/cli/commands/workspace/materialize.ts +0 -46
- package/src/cli/commands/workspace/workspace.ts +0 -22
- package/src/hooks/artifact-finish-nudge.ts +0 -244
- package/src/lib/orchestrator/headroom/proxy.ts +0 -116
- package/src/lib/orchestrator/workspace-materialize.ts +0 -80
- package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/SKILL.md +0 -51
- package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/references/openspec.md +0 -134
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +0 -58
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +0 -139
- package/src/tools/setup/capabilities/headroom.ts +0 -57
- /package/src/cli/commands/{workspace → companion}/open.ts +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** @jsxImportSource hono/jsx */
|
|
2
|
+
|
|
3
|
+
import type { StudioCompanionPayload } from "../../payload";
|
|
4
|
+
import { Warnings } from "../warnings";
|
|
5
|
+
import { Changes } from "./changes";
|
|
6
|
+
|
|
7
|
+
interface DashboardProps {
|
|
8
|
+
payload: StudioCompanionPayload;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Changes only: specs and skills have their own views. */
|
|
12
|
+
export function Dashboard({ payload }: DashboardProps) {
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
<Changes changes={payload.changes} />
|
|
16
|
+
<Warnings warnings={payload.warnings} />
|
|
17
|
+
</>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/** @jsxImportSource hono/jsx */
|
|
2
|
+
|
|
3
|
+
import { companionDigest, REFRESH_PARAM, type StudioSelection } from "../selection";
|
|
4
|
+
import { STUDIO_CLIENT_SCRIPT, STUDIO_PREPAINT_SCRIPT } from "./client";
|
|
5
|
+
import { CompanionPicker } from "./companion-picker";
|
|
6
|
+
import { CompanionSelector } from "./companion-selector";
|
|
7
|
+
import { Dashboard } from "./dashboard/index";
|
|
8
|
+
import { CompanionError } from "./error";
|
|
9
|
+
import { formatCollectedAt, type StudioPage } from "./model";
|
|
10
|
+
import { Specs } from "./specs/index";
|
|
11
|
+
import { STUDIO_STYLES } from "./styles";
|
|
12
|
+
import { Skills } from "./skills/index";
|
|
13
|
+
import { Workflow } from "./workflow/index";
|
|
14
|
+
|
|
15
|
+
const STUDIO_TITLE = "Mate Studio";
|
|
16
|
+
|
|
17
|
+
const VIEW_DETAILS = {
|
|
18
|
+
dashboard: {
|
|
19
|
+
eyebrow: "Companion lookup",
|
|
20
|
+
title: "Overview",
|
|
21
|
+
},
|
|
22
|
+
specs: {
|
|
23
|
+
eyebrow: "Companion lookup",
|
|
24
|
+
title: "Specs",
|
|
25
|
+
},
|
|
26
|
+
workflow: {
|
|
27
|
+
eyebrow: "Companion lookup",
|
|
28
|
+
title: "Workflow",
|
|
29
|
+
},
|
|
30
|
+
skills: {
|
|
31
|
+
eyebrow: "Agent guidance",
|
|
32
|
+
title: "Skills",
|
|
33
|
+
},
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* `<style>` and `<script>` are HTML raw-text elements: a character reference
|
|
38
|
+
* inside one is not decoded, so escaped content would be broken CSS and broken
|
|
39
|
+
* JavaScript. They carry authored source only — never a payload value — and are
|
|
40
|
+
* emitted as literal markup here, which is why no view ever reaches for an
|
|
41
|
+
* escape hatch around the renderer's escaping.
|
|
42
|
+
*/
|
|
43
|
+
export function renderStudioDocument(page: StudioPage): string {
|
|
44
|
+
return `<!doctype html>
|
|
45
|
+
<html lang="en">
|
|
46
|
+
<head>
|
|
47
|
+
<meta charset="utf-8">
|
|
48
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
49
|
+
<title>${STUDIO_TITLE}</title>
|
|
50
|
+
<style>${STUDIO_STYLES}</style>
|
|
51
|
+
<script>${STUDIO_PREPAINT_SCRIPT}</script>
|
|
52
|
+
</head>
|
|
53
|
+
<body>${String(<StudioShell page={page} />)}
|
|
54
|
+
<script>${STUDIO_CLIENT_SCRIPT}</script>
|
|
55
|
+
</body>
|
|
56
|
+
</html>
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* `data-companion` names the resolved companion only: the browser stores what
|
|
62
|
+
* it can be served again, never a digest the server just failed to resolve.
|
|
63
|
+
*/
|
|
64
|
+
function StudioShell({ page }: { page: StudioPage }) {
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
<div
|
|
68
|
+
className="shell"
|
|
69
|
+
data-companion={page.companion ? companionDigest(page.companion.path) : undefined}
|
|
70
|
+
>
|
|
71
|
+
<Sidebar page={page} />
|
|
72
|
+
<main className="main">
|
|
73
|
+
{page.companion ? <PageHeader page={page} /> : null}
|
|
74
|
+
<Content page={page} />
|
|
75
|
+
</main>
|
|
76
|
+
</div>
|
|
77
|
+
<div className="toast" id="studio-toast" data-shown="false" />
|
|
78
|
+
</>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function Content({ page }: { page: StudioPage }) {
|
|
83
|
+
if (!page.companion) {
|
|
84
|
+
return <CompanionPicker inventory={page.inventory} selection={page.selection} />;
|
|
85
|
+
}
|
|
86
|
+
if (page.error) {
|
|
87
|
+
return <CompanionError companionPath={page.error.companionPath} reason={page.error.reason} />;
|
|
88
|
+
}
|
|
89
|
+
if (!page.payload) {
|
|
90
|
+
return (
|
|
91
|
+
<section className="panel">
|
|
92
|
+
<p className="empty">No state collected for this companion.</p>
|
|
93
|
+
</section>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (page.selection.view === "workflow") {
|
|
97
|
+
return <Workflow payload={page.payload} />;
|
|
98
|
+
}
|
|
99
|
+
if (page.selection.view === "specs") return <Specs payload={page.payload} />;
|
|
100
|
+
if (page.selection.view === "skills") return <Skills skills={page.payload.skillInventory} />;
|
|
101
|
+
return <Dashboard payload={page.payload} />;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function Sidebar({ page }: { page: StudioPage }) {
|
|
105
|
+
return (
|
|
106
|
+
<aside className="rail studio-sidebar" id="studio-rail">
|
|
107
|
+
<div className="sidebar-brand">
|
|
108
|
+
<span className="sidebar-brand-mark">M</span>
|
|
109
|
+
<div className="sidebar-brand-copy">
|
|
110
|
+
<strong>{STUDIO_TITLE}</strong>
|
|
111
|
+
<span>Developer lookup</span>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
<CompanionSelector inventory={page.inventory} selection={page.selection} />
|
|
115
|
+
<ViewNav selection={page.selection} />
|
|
116
|
+
<Metrics page={page} />
|
|
117
|
+
<Footer page={page} />
|
|
118
|
+
</aside>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function PageHeader({ page }: { page: StudioPage }) {
|
|
123
|
+
if (!page.companion) return null;
|
|
124
|
+
|
|
125
|
+
const details = VIEW_DETAILS[page.selection.view];
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<header className="page-header">
|
|
129
|
+
<div className="page-heading">
|
|
130
|
+
<p className="page-eyebrow">{details.eyebrow}</p>
|
|
131
|
+
<h1>{details.title}</h1>
|
|
132
|
+
</div>
|
|
133
|
+
<div className="page-context">
|
|
134
|
+
<span className="page-context-label">Companion Repository</span>
|
|
135
|
+
<code>{page.companion.path}</code>
|
|
136
|
+
<span className="snapshot-status">
|
|
137
|
+
<span className="snapshot-dot" aria-hidden="true" />
|
|
138
|
+
{page.collectedAt === null
|
|
139
|
+
? "No snapshot"
|
|
140
|
+
: `Snapshot ${formatCollectedAt(page.collectedAt)}`}
|
|
141
|
+
</span>
|
|
142
|
+
</div>
|
|
143
|
+
</header>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Submit buttons, so switching a view is a navigation to the URL naming it and
|
|
149
|
+
* the browser's history moves between rendered states.
|
|
150
|
+
*/
|
|
151
|
+
function ViewNav({ selection }: { selection: StudioSelection }) {
|
|
152
|
+
return (
|
|
153
|
+
<nav className="sidebar-nav" aria-label="Studio views">
|
|
154
|
+
<span className="sidebar-label">Views</span>
|
|
155
|
+
<form method="get" action="/">
|
|
156
|
+
<SelectionFields selection={selection} omit="view" />
|
|
157
|
+
<div className="sidebar-nav-list">
|
|
158
|
+
<button
|
|
159
|
+
type="submit"
|
|
160
|
+
name="view"
|
|
161
|
+
value="dashboard"
|
|
162
|
+
aria-pressed={selection.view === "dashboard"}
|
|
163
|
+
>
|
|
164
|
+
<span>Overview</span>
|
|
165
|
+
</button>
|
|
166
|
+
<button type="submit" name="view" value="specs" aria-pressed={selection.view === "specs"}>
|
|
167
|
+
<span>Specs</span>
|
|
168
|
+
</button>
|
|
169
|
+
<button
|
|
170
|
+
type="submit"
|
|
171
|
+
name="view"
|
|
172
|
+
value="workflow"
|
|
173
|
+
aria-pressed={selection.view === "workflow"}
|
|
174
|
+
>
|
|
175
|
+
<span>Workflow</span>
|
|
176
|
+
</button>
|
|
177
|
+
<button
|
|
178
|
+
type="submit"
|
|
179
|
+
name="view"
|
|
180
|
+
value="skills"
|
|
181
|
+
aria-pressed={selection.view === "skills"}
|
|
182
|
+
>
|
|
183
|
+
<span>Skills</span>
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
</form>
|
|
187
|
+
</nav>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function Metrics({ page }: { page: StudioPage }) {
|
|
192
|
+
const payload = page.payload;
|
|
193
|
+
if (!payload) return null;
|
|
194
|
+
const metrics: [string, number][] = [
|
|
195
|
+
["changes", payload.changes.length],
|
|
196
|
+
["done", payload.changes.filter((change) => change.status === "complete").length],
|
|
197
|
+
["specs", payload.specs.length],
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div className="sidebar-metrics" id="studio-stats">
|
|
202
|
+
{metrics.map(([label, value]) => (
|
|
203
|
+
<div key={label} className="sidebar-metric">
|
|
204
|
+
<strong>{value}</strong>
|
|
205
|
+
<span>{label}</span>
|
|
206
|
+
</div>
|
|
207
|
+
))}
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The refresh control is the only thing that collects again, so it is the only
|
|
214
|
+
* control that names the refresh parameter.
|
|
215
|
+
*/
|
|
216
|
+
function Footer({ page }: { page: StudioPage }) {
|
|
217
|
+
return (
|
|
218
|
+
<div className="sidebar-footer">
|
|
219
|
+
<div className="sidebar-footer-row">
|
|
220
|
+
<span className="muted">
|
|
221
|
+
{page.collectedAt === null
|
|
222
|
+
? "no state collected"
|
|
223
|
+
: `state as of ${formatCollectedAt(page.collectedAt)}`}
|
|
224
|
+
</span>
|
|
225
|
+
<form method="get" action="/">
|
|
226
|
+
<SelectionFields selection={page.selection} />
|
|
227
|
+
<input type="hidden" name={REFRESH_PARAM} value="1" />
|
|
228
|
+
<button type="submit">Refresh</button>
|
|
229
|
+
</form>
|
|
230
|
+
</div>
|
|
231
|
+
<button id="studio-theme" type="button" data-theme-state="system">
|
|
232
|
+
Theme: system
|
|
233
|
+
</button>
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Carries the parts of the selection a control is not itself changing. */
|
|
239
|
+
function SelectionFields({
|
|
240
|
+
selection,
|
|
241
|
+
omit,
|
|
242
|
+
}: {
|
|
243
|
+
selection: StudioSelection;
|
|
244
|
+
omit?: keyof StudioSelection;
|
|
245
|
+
}) {
|
|
246
|
+
return (
|
|
247
|
+
<>
|
|
248
|
+
{selection.companionDigest && omit !== "companionDigest" ? (
|
|
249
|
+
<input type="hidden" name="companion" value={selection.companionDigest} />
|
|
250
|
+
) : null}
|
|
251
|
+
{selection.view !== "dashboard" && omit !== "view" ? (
|
|
252
|
+
<input type="hidden" name="view" value={selection.view} />
|
|
253
|
+
) : null}
|
|
254
|
+
</>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** @jsxImportSource hono/jsx */
|
|
2
|
+
|
|
3
|
+
interface CompanionErrorProps {
|
|
4
|
+
companionPath: string;
|
|
5
|
+
reason: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A companion that could not be read costs its content, never the page: the
|
|
10
|
+
* selector stays rendered around this so another companion can be reached.
|
|
11
|
+
*/
|
|
12
|
+
export function CompanionError({ companionPath, reason }: CompanionErrorProps) {
|
|
13
|
+
return (
|
|
14
|
+
<section className="panel error">
|
|
15
|
+
<h3>Could not read this companion</h3>
|
|
16
|
+
<p className="mono">{companionPath}</p>
|
|
17
|
+
<p>{reason}</p>
|
|
18
|
+
</section>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { StudioInventory, StudioInventoryCompanion } from "../inventory";
|
|
2
|
+
import type { StudioCompanionPayload } from "../payload";
|
|
3
|
+
import type { StudioSelection } from "../selection";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Everything one rendered document is derived from. `payload` and `error` are
|
|
7
|
+
* mutually exclusive and both are absent until a companion is named, and
|
|
8
|
+
* `collectedAt` is the moment the served snapshot was collected — not the
|
|
9
|
+
* moment of this request.
|
|
10
|
+
*/
|
|
11
|
+
export interface StudioPage {
|
|
12
|
+
inventory: StudioInventory;
|
|
13
|
+
selection: StudioSelection;
|
|
14
|
+
companion: StudioInventoryCompanion | null;
|
|
15
|
+
payload: StudioCompanionPayload | null;
|
|
16
|
+
error: { companionPath: string; reason: string } | null;
|
|
17
|
+
collectedAt: number | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Local wall-clock time of a snapshot. Studio serves the browser that runs on
|
|
22
|
+
* the same machine, so the server's local time is the reader's.
|
|
23
|
+
*/
|
|
24
|
+
export function formatCollectedAt(collectedAt: number): string {
|
|
25
|
+
const at = new Date(collectedAt);
|
|
26
|
+
return [at.getHours(), at.getMinutes(), at.getSeconds()]
|
|
27
|
+
.map((part) => String(part).padStart(2, "0"))
|
|
28
|
+
.join(":");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function healthNote(companion: StudioInventoryCompanion): string {
|
|
32
|
+
if (companion.health === "ready") return "";
|
|
33
|
+
return companion.diagnostic ? `${companion.health} — ${companion.diagnostic}` : companion.health;
|
|
34
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** @jsxImportSource hono/jsx */
|
|
2
|
+
|
|
3
|
+
import type { StudioInventoryCompanion } from "../inventory";
|
|
4
|
+
|
|
5
|
+
interface PairingsProps {
|
|
6
|
+
companion: StudioInventoryCompanion | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Context, never a selection: everything Studio serves is companion-scoped, so
|
|
11
|
+
* a Working Repository is listed and never chosen.
|
|
12
|
+
*/
|
|
13
|
+
export function Pairings({ companion }: PairingsProps) {
|
|
14
|
+
const pairings = companion?.pairings ?? [];
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="sidebar-pairings">
|
|
18
|
+
<span className="sidebar-label">Working repositories</span>
|
|
19
|
+
{pairings.length === 0 ? (
|
|
20
|
+
<p className="empty">no linked working repository</p>
|
|
21
|
+
) : (
|
|
22
|
+
<ul className="plain pairings">
|
|
23
|
+
{pairings.map((pairing) => (
|
|
24
|
+
<li key={pairing.repositoryPath} data-repository={pairing.repositoryPath}>
|
|
25
|
+
<span className="mono">{pairing.repositoryId}</span>
|
|
26
|
+
{pairing.ambiguous ? <span className="chip">also paired elsewhere</span> : null}
|
|
27
|
+
{pairing.health === "ready" ? null : (
|
|
28
|
+
<span className="chip chip-invalid">{pairing.health}</span>
|
|
29
|
+
)}
|
|
30
|
+
<br />
|
|
31
|
+
<span className="muted">{pairing.repositoryPath}</span>
|
|
32
|
+
</li>
|
|
33
|
+
))}
|
|
34
|
+
</ul>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** @jsxImportSource hono/jsx */
|
|
2
|
+
|
|
3
|
+
import type { StudioSkillInventory } from "../../mate-inventory";
|
|
4
|
+
|
|
5
|
+
const PROFILES = [
|
|
6
|
+
{ id: "claude", label: "Claude", description: "Skills available to Claude agents." },
|
|
7
|
+
{ id: "opencode", label: "OpenCode", description: "Skills available to OpenCode." },
|
|
8
|
+
{ id: "agents", label: "Agents", description: "Shared skills available to all agents." },
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
interface SkillsProps {
|
|
12
|
+
skills?: StudioSkillInventory;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Lists the skills from each runtime tree without merging away runtime-specific entries. */
|
|
16
|
+
export function Skills({ skills }: SkillsProps) {
|
|
17
|
+
const inventory = skills ?? { claude: [], opencode: [], agents: [] };
|
|
18
|
+
const total = inventory.claude.length + inventory.opencode.length + inventory.agents.length;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<section className="panel skills-panel">
|
|
22
|
+
<div className="section-header">
|
|
23
|
+
<div>
|
|
24
|
+
<h3>Agent Skills</h3>
|
|
25
|
+
<p className="section-note">Every skill installed in this Companion Repository.</p>
|
|
26
|
+
</div>
|
|
27
|
+
<span className="section-count">
|
|
28
|
+
{total} {total === 1 ? "skill" : "skills"}
|
|
29
|
+
</span>
|
|
30
|
+
</div>
|
|
31
|
+
<div className="skills-tabs" data-skills-root>
|
|
32
|
+
<div className="skills-tab-list" role="tablist" aria-label="Skill source">
|
|
33
|
+
{PROFILES.map((profile, index) => {
|
|
34
|
+
const active = index === 0;
|
|
35
|
+
const names = inventory[profile.id];
|
|
36
|
+
return (
|
|
37
|
+
<button
|
|
38
|
+
key={profile.id}
|
|
39
|
+
type="button"
|
|
40
|
+
role="tab"
|
|
41
|
+
id={`skills-tab-${profile.id}`}
|
|
42
|
+
aria-selected={active}
|
|
43
|
+
aria-controls={`skills-panel-${profile.id}`}
|
|
44
|
+
data-skill-profile={profile.id}
|
|
45
|
+
>
|
|
46
|
+
<strong>{profile.label}</strong>
|
|
47
|
+
<span>{names.length} skills</span>
|
|
48
|
+
</button>
|
|
49
|
+
);
|
|
50
|
+
})}
|
|
51
|
+
</div>
|
|
52
|
+
<div className="skills-panels">
|
|
53
|
+
{PROFILES.map((profile, index) => {
|
|
54
|
+
const active = index === 0;
|
|
55
|
+
const names = inventory[profile.id];
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
key={profile.id}
|
|
59
|
+
id={`skills-panel-${profile.id}`}
|
|
60
|
+
role="tabpanel"
|
|
61
|
+
aria-labelledby={`skills-tab-${profile.id}`}
|
|
62
|
+
aria-hidden={!active}
|
|
63
|
+
data-skill-panel={profile.id}
|
|
64
|
+
data-active={active}
|
|
65
|
+
tabIndex={0}
|
|
66
|
+
>
|
|
67
|
+
<p className="skills-panel-description">{profile.description}</p>
|
|
68
|
+
{names.length === 0 ? (
|
|
69
|
+
<p className="empty">No {profile.label} skills in this companion.</p>
|
|
70
|
+
) : (
|
|
71
|
+
<div className="skills-grid">
|
|
72
|
+
{names.map((name) => (
|
|
73
|
+
<div key={name} className="skill-card">
|
|
74
|
+
<code>{name}</code>
|
|
75
|
+
<span>/{name}</span>
|
|
76
|
+
</div>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
})}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</section>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/** @jsxImportSource hono/jsx */
|
|
2
|
+
|
|
3
|
+
import type { StudioCompanionPayload, StudioSpec } from "../../payload";
|
|
4
|
+
import { Warnings } from "../warnings";
|
|
5
|
+
|
|
6
|
+
const UNASSIGNED_AREA = "unassigned";
|
|
7
|
+
|
|
8
|
+
/** A spec binds one Area or several, so it appears under each of them. */
|
|
9
|
+
export function groupSpecsByArea(specs: StudioSpec[]): [string, StudioSpec[]][] {
|
|
10
|
+
const groups = new Map<string, StudioSpec[]>();
|
|
11
|
+
for (const spec of specs) {
|
|
12
|
+
const areas = spec.areas.length > 0 ? spec.areas : [UNASSIGNED_AREA];
|
|
13
|
+
for (const area of areas) {
|
|
14
|
+
const existing = groups.get(area);
|
|
15
|
+
if (existing) existing.push(spec);
|
|
16
|
+
else groups.set(area, [spec]);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return [...groups.entries()].toSorted((left, right) => left[0].localeCompare(right[0]));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function specStatus(spec: StudioSpec): { label: string; invalid: boolean } {
|
|
23
|
+
if (spec.valid === false) return { label: `${spec.issueCount ?? 0} issues`, invalid: true };
|
|
24
|
+
if (spec.valid === true) return { label: "valid", invalid: false };
|
|
25
|
+
return { label: "unvalidated", invalid: false };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface SpecsProps {
|
|
29
|
+
payload: StudioCompanionPayload;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Its own view: an Area map is read on its own, not alongside the changes. */
|
|
33
|
+
export function Specs({ payload }: SpecsProps) {
|
|
34
|
+
return (
|
|
35
|
+
<>
|
|
36
|
+
<SpecsByArea specs={payload.specs} />
|
|
37
|
+
<Warnings warnings={payload.warnings} />
|
|
38
|
+
</>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function SpecsByArea({ specs }: { specs: StudioSpec[] }) {
|
|
43
|
+
const groups = groupSpecsByArea(specs);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<section className="panel">
|
|
47
|
+
<div className="section-header">
|
|
48
|
+
<div>
|
|
49
|
+
<h3>Specs by Area</h3>
|
|
50
|
+
<p className="section-note">Capabilities grouped by the Areas they serve.</p>
|
|
51
|
+
</div>
|
|
52
|
+
<span className="section-count">
|
|
53
|
+
{specs.length} {specs.length === 1 ? "spec" : "specs"}
|
|
54
|
+
</span>
|
|
55
|
+
</div>
|
|
56
|
+
{groups.length === 0 ? (
|
|
57
|
+
<p className="empty">No specs in this companion.</p>
|
|
58
|
+
) : (
|
|
59
|
+
<div className="specs-area-grid">
|
|
60
|
+
{groups.map(([area, areaSpecs], index) => (
|
|
61
|
+
<section key={area} className="specs-area-card" aria-label={`Area ${area}`}>
|
|
62
|
+
<div className="specs-area-card-head">
|
|
63
|
+
<div className="specs-area-name">
|
|
64
|
+
<span className="specs-area-number">{String(index + 1).padStart(2, "0")}</span>
|
|
65
|
+
<h4>{area}</h4>
|
|
66
|
+
</div>
|
|
67
|
+
<span className="specs-area-count">{`${areaSpecs.length} specs`}</span>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="specs-card-list">
|
|
70
|
+
{areaSpecs.map((spec) => {
|
|
71
|
+
const status = specStatus(spec);
|
|
72
|
+
return (
|
|
73
|
+
<div key={spec.capability} className="spec-card-row">
|
|
74
|
+
<div>
|
|
75
|
+
<strong>{spec.capability}</strong>
|
|
76
|
+
<div className="spec-card-meta">
|
|
77
|
+
<span>
|
|
78
|
+
{spec.requirementCount === undefined
|
|
79
|
+
? "requirements unknown"
|
|
80
|
+
: `${spec.requirementCount} requirements`}
|
|
81
|
+
</span>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<span
|
|
85
|
+
className={
|
|
86
|
+
status.invalid ? "spec-status spec-status-invalid" : "spec-status"
|
|
87
|
+
}
|
|
88
|
+
>
|
|
89
|
+
{status.label}
|
|
90
|
+
</span>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
})}
|
|
94
|
+
</div>
|
|
95
|
+
</section>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</section>
|
|
100
|
+
);
|
|
101
|
+
}
|