@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.
Files changed (209) hide show
  1. package/claude-plugin/hooks/companion-instructions.mjs +9 -0
  2. package/claude-plugin/hooks/hooks.json +12 -2
  3. package/package.json +10 -2
  4. package/src/brand/art.generated.ts +69 -0
  5. package/src/brand/index.ts +12 -0
  6. package/src/brand/palette.ts +19 -0
  7. package/src/brand/svg.generated.ts +8 -0
  8. package/src/brand/terminal.ts +83 -0
  9. package/src/brand/types.ts +33 -0
  10. package/src/cli/commands/artifact/artifact.ts +8 -0
  11. package/src/cli/commands/artifact/delivery/archive.ts +117 -0
  12. package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
  13. package/src/cli/commands/artifact/delivery/environment.ts +104 -0
  14. package/src/cli/commands/artifact/delivery/link.ts +154 -0
  15. package/src/cli/commands/artifact/delivery/output.ts +58 -0
  16. package/src/cli/commands/artifact/finish/command.ts +33 -3
  17. package/src/cli/commands/artifact/finish/engine.ts +47 -3
  18. package/src/cli/commands/artifact/finish/finisher.ts +10 -0
  19. package/src/cli/commands/artifact/finish/openspec.ts +177 -138
  20. package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
  21. package/src/cli/commands/artifact/pending/command.ts +31 -3
  22. package/src/cli/commands/artifact/pending/discovery.ts +21 -57
  23. package/src/cli/commands/cap/graphify.ts +52 -3
  24. package/src/cli/commands/cap/index-cmd.ts +8 -1
  25. package/src/cli/commands/cap/openspec.ts +13 -25
  26. package/src/cli/commands/companion/open.ts +56 -39
  27. package/src/cli/commands/companion/update.ts +75 -42
  28. package/src/cli/commands/doctor.ts +176 -3
  29. package/src/cli/commands/plugin/install.ts +9 -4
  30. package/src/cli/commands/plugin/plugin.ts +3 -2
  31. package/src/cli/commands/report/adapter.ts +11 -2
  32. package/src/cli/commands/report/contract.ts +21 -0
  33. package/src/cli/commands/report/highlight.ts +34 -4
  34. package/src/cli/commands/report/markdown.ts +110 -0
  35. package/src/cli/commands/report/palette.ts +118 -0
  36. package/src/cli/commands/report/renderer.ts +456 -153
  37. package/src/cli/commands/report/types.ts +16 -0
  38. package/src/cli/commands/setup.ts +7 -15
  39. package/src/cli/commands/studio/client-asset.ts +16 -0
  40. package/src/cli/commands/studio/openspec-cli.ts +10 -58
  41. package/src/cli/commands/studio/routes.ts +2 -0
  42. package/src/cli/commands/studio/selection.ts +19 -1
  43. package/src/cli/commands/studio/server.ts +42 -1
  44. package/src/cli/commands/studio/views/client.ts +6 -616
  45. package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
  46. package/src/cli/commands/studio/views/document.tsx +137 -69
  47. package/src/cli/commands/studio/views/markdown.tsx +250 -0
  48. package/src/cli/commands/studio/views/palette.ts +134 -0
  49. package/src/cli/commands/studio/views/reports/index.tsx +2 -7
  50. package/src/cli/commands/studio/views/skills/index.tsx +43 -62
  51. package/src/cli/commands/studio/views/specs/index.tsx +7 -15
  52. package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
  53. package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
  54. package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
  55. package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
  56. package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
  57. package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
  58. package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
  59. package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
  60. package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
  61. package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
  62. package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
  63. package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
  64. package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
  65. package/src/cli/commands/studio/views/styles.ts +217 -206
  66. package/src/cli/commands/studio/views/vault/state.ts +3 -5
  67. package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
  68. package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
  69. package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
  70. package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
  71. package/src/cli/commands/wrap.ts +5 -13
  72. package/src/cli/main.ts +4 -1
  73. package/src/cli/parse-flags.ts +25 -7
  74. package/src/cli/setup-selector.tsx +38 -162
  75. package/src/cli/usage.ts +4 -0
  76. package/src/hooks/companion-instructions.ts +190 -0
  77. package/src/hooks/hook-io.ts +63 -0
  78. package/src/hooks/session-banner.ts +21 -29
  79. package/src/hooks/session-guidance.ts +9 -17
  80. package/src/hooks/session-prepare.ts +115 -28
  81. package/src/hooks/validate-artifact-path.ts +82 -24
  82. package/src/index.ts +4 -0
  83. package/src/lib/change-layout.ts +154 -0
  84. package/src/lib/components/wizard-header.tsx +3 -1
  85. package/src/lib/context-mode-package.ts +24 -11
  86. package/src/lib/delivery/archive-change.ts +154 -0
  87. package/src/lib/delivery/branches.ts +108 -0
  88. package/src/lib/delivery/environment.ts +29 -0
  89. package/src/lib/delivery/forge.ts +279 -0
  90. package/src/lib/delivery/overlap.ts +202 -0
  91. package/src/lib/delivery/record-delivery.ts +196 -0
  92. package/src/lib/delivery/record.ts +343 -0
  93. package/src/lib/delivery/refresh.ts +174 -0
  94. package/src/lib/delivery/repository-access.ts +97 -0
  95. package/src/lib/delivery/stages.ts +111 -0
  96. package/src/lib/delivery/token.ts +58 -0
  97. package/src/lib/frontmatter.ts +18 -0
  98. package/src/lib/opencode-plugin-package.ts +5 -5
  99. package/src/lib/openspec-cli.ts +116 -0
  100. package/src/lib/openspec-scopes.ts +86 -0
  101. package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
  102. package/src/lib/orchestrator/adapters/claude.ts +48 -40
  103. package/src/lib/orchestrator/adapters/opencode.ts +2 -0
  104. package/src/lib/orchestrator/editor.ts +9 -9
  105. package/src/lib/orchestrator/openspec-store.ts +14 -54
  106. package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
  107. package/src/lib/orchestrator/projection-claude-links.ts +159 -0
  108. package/src/lib/orchestrator/projection-claude-skills.ts +19 -137
  109. package/src/lib/orchestrator/projection-entries.ts +45 -14
  110. package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
  111. package/src/lib/orchestrator/projection-types.ts +3 -0
  112. package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
  113. package/src/lib/orchestrator/session-prepare.ts +45 -5
  114. package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
  115. package/src/lib/orchestrator/types.ts +2 -2
  116. package/src/lib/orchestrator/wrap-pass.ts +22 -1
  117. package/src/lib/package-paths.ts +8 -0
  118. package/src/opencode/companion-hooks.ts +64 -5
  119. package/src/opencode/companion-policy.ts +37 -18
  120. package/src/opencode/projected-guidance.ts +4 -1
  121. package/src/opencode/session-prepare.ts +2 -1
  122. package/src/opencode/tui.tsx +4 -2
  123. package/src/runtime/artifact-refusal.ts +63 -0
  124. package/src/runtime/claude-delivery-plan.ts +89 -7
  125. package/src/runtime/commit-attribution-guard.ts +181 -0
  126. package/src/runtime/companion-guidance.ts +15 -3
  127. package/src/runtime/companion-instructions.ts +340 -0
  128. package/src/runtime/error-message.ts +4 -0
  129. package/src/runtime/index.ts +5 -0
  130. package/src/runtime/managed-block.ts +117 -0
  131. package/src/runtime/markdown-scan.ts +266 -0
  132. package/src/runtime/policy.ts +15 -1
  133. package/src/runtime/projected-flag.ts +7 -0
  134. package/src/runtime/projected-guidance.ts +1 -0
  135. package/src/runtime/runtime-document-manifest.ts +62 -11
  136. package/src/runtime/secret-guard.ts +148 -0
  137. package/src/runtime/secret-patterns.ts +164 -0
  138. package/src/runtime/secret-scan.ts +90 -0
  139. package/src/runtime/session-kind.ts +13 -0
  140. package/src/runtime/shell-heredoc.ts +81 -0
  141. package/src/runtime/wrap-state.ts +36 -2
  142. package/src/runtime/write-atomic.ts +64 -0
  143. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
  144. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
  145. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
  146. package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
  147. package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
  148. package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
  149. package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
  150. package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
  151. package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
  152. package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
  153. package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
  154. package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
  155. package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
  156. package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
  157. package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
  158. package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
  159. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
  160. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
  161. package/src/templates/mate-skills/{claude/mate-artifact-publish → agents/mate-publish-artifact}/SKILL.md +91 -23
  162. package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  163. package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
  164. package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
  165. package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
  166. package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
  167. package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
  168. package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
  169. package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
  170. package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
  171. package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
  172. package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
  173. package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
  174. package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
  175. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
  176. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
  177. package/src/templates/mate-skills/{agents/mate-artifact-publish → claude/mate-publish-artifact}/SKILL.md +91 -23
  178. package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  179. package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
  180. package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
  181. package/src/templates/mate-skills/openspec-references.ts +140 -0
  182. package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
  183. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
  184. package/src/tools/setup/capabilities/context-mode.ts +25 -5
  185. package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
  186. package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
  187. package/src/tools/setup/capabilities/graphify.ts +12 -0
  188. package/src/tools/setup/capabilities/openspec.ts +151 -270
  189. package/src/tools/setup/capabilities/tokensave.ts +36 -3
  190. package/src/tools/setup/context-services.ts +9 -17
  191. package/src/tools/setup/dynamic-plugins/install.ts +24 -28
  192. package/src/tools/setup/engine.ts +97 -21
  193. package/src/tools/setup/managed-output.ts +51 -0
  194. package/src/tools/setup/mate.ts +86 -11
  195. package/src/tools/setup/plugin.ts +35 -0
  196. package/src/tools/setup/plugins/guidance.ts +7 -4
  197. package/src/tools/setup/providers/agent-file-sections.ts +4 -2
  198. package/src/tools/setup/providers/claude-attribution.ts +65 -0
  199. package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
  200. package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
  201. package/src/tools/setup/providers/claude.ts +284 -8
  202. package/src/tools/setup/providers/opencode.ts +180 -1
  203. package/src/tools/setup/utils.ts +14 -4
  204. package/src/tools/setup.ts +126 -10
  205. package/wrappers/bin/graphify +52 -4
  206. package/wrappers/bin/openspec +33 -9
  207. package/wrappers/lib/graphify-gate.mjs +8 -0
  208. package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
  209. package/src/tools/setup/runtime-documents.ts +0 -291
@@ -0,0 +1,134 @@
1
+ import { DARK_PALETTE, LIGHT_PALETTE, type ReportPalette } from "../../report/palette";
2
+
3
+ /**
4
+ * Studio's colour tokens, built from the report palette so Studio and its hosted
5
+ * reports share the Mate brand colours. Token names stay Studio's own; the values
6
+ * come from `ReportPalette`, plus Studio-only label colours for workflow badges.
7
+ */
8
+
9
+ export interface StudioPalette {
10
+ bg: string;
11
+ bgRail: string;
12
+ panel: string;
13
+ /** Nested boxes inside a card: the page colour keeps light accent text at 5:1. */
14
+ panelSoft: string;
15
+ border: string;
16
+ text: string;
17
+ muted: string;
18
+ accent: string;
19
+ done: string;
20
+ warn: string;
21
+ bad: string;
22
+ info: string;
23
+ what: string;
24
+ why: string;
25
+ /** Text drawn on an accent or success fill (toast, nav badge). */
26
+ onAccent: string;
27
+ promptBg: string;
28
+ promptText: string;
29
+ /** Only the off-canvas terminal drawer floats over the page. */
30
+ drawerShadow: string;
31
+ }
32
+
33
+ /** Colours read as text or status markers; each must reach 4.5:1 on the page and on cards. */
34
+ export const STUDIO_TEXT_TOKENS = [
35
+ "text",
36
+ "muted",
37
+ "accent",
38
+ "done",
39
+ "warn",
40
+ "bad",
41
+ "info",
42
+ "what",
43
+ "why",
44
+ ] as const satisfies readonly (keyof StudioPalette)[];
45
+
46
+ /** Each of these also gets a `--<name>-soft` tint: this share of the colour over the card surface. */
47
+ const TINTED_TOKENS = [
48
+ "accent",
49
+ "done",
50
+ "warn",
51
+ "bad",
52
+ "info",
53
+ "what",
54
+ "why",
55
+ ] as const satisfies readonly (keyof StudioPalette)[];
56
+
57
+ export const TINT_PERCENT = 12;
58
+
59
+ function fromReport(
60
+ report: ReportPalette,
61
+ studio: Pick<StudioPalette, "what" | "why" | "onAccent" | "drawerShadow">,
62
+ ): StudioPalette {
63
+ return {
64
+ bg: report.bg,
65
+ bgRail: report.surface,
66
+ panel: report.surface,
67
+ panelSoft: report.bg,
68
+ border: report.rule,
69
+ text: report.ink,
70
+ muted: report.muted,
71
+ accent: report.accent,
72
+ done: report.ok,
73
+ warn: report.warn,
74
+ bad: report.error,
75
+ info: report.info,
76
+ promptBg: report.bg,
77
+ promptText: report.ink,
78
+ ...studio,
79
+ };
80
+ }
81
+
82
+ export const STUDIO_LIGHT_PALETTE = fromReport(LIGHT_PALETTE, {
83
+ what: "#176B75",
84
+ why: "#6346A5",
85
+ onAccent: "#FFFFFF",
86
+ drawerShadow: "0 12px 32px rgb(31 36 33 / 12%)",
87
+ });
88
+
89
+ export const STUDIO_DARK_PALETTE = fromReport(DARK_PALETTE, {
90
+ what: "#74D3D8",
91
+ why: "#CAA8FF",
92
+ onAccent: DARK_PALETTE.bg,
93
+ drawerShadow: "0 14px 36px rgb(0 0 0 / 40%)",
94
+ });
95
+
96
+ const kebab = (name: string): string => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
97
+
98
+ function declarations(palette: StudioPalette, indent: string): string {
99
+ const lines = Object.entries(palette).map(([name, value]) => `--${kebab(name)}: ${value};`);
100
+ lines.push("--border-soft: color-mix(in srgb, var(--border) 55%, var(--panel));");
101
+ for (const token of TINTED_TOKENS) {
102
+ lines.push(
103
+ `--${token}-soft: color-mix(in srgb, var(--${token}) ${TINT_PERCENT}%, var(--panel));`,
104
+ );
105
+ }
106
+ return lines.map((line) => `${indent}${line}`).join("\n");
107
+ }
108
+
109
+ /**
110
+ * Dark is the default; light applies through the system preference or the in-page
111
+ * toggle, and an explicit `data-theme` wins over the media query in both directions.
112
+ */
113
+ export function studioPaletteCss(): string {
114
+ return `:root {
115
+ color-scheme: dark;
116
+ ${declarations(STUDIO_DARK_PALETTE, " ")}
117
+ }
118
+ @media (prefers-color-scheme: light) {
119
+ :root:not([data-theme="dark"]) {
120
+ color-scheme: light;
121
+ ${declarations(STUDIO_LIGHT_PALETTE, " ")}
122
+ }
123
+ }
124
+ @media (prefers-color-scheme: dark) {
125
+ :root:not([data-theme="light"]) { color-scheme: dark; }
126
+ }
127
+ :root[data-theme="light"] {
128
+ color-scheme: light;
129
+ ${declarations(STUDIO_LIGHT_PALETTE, " ")}
130
+ }
131
+ :root[data-theme="dark"] {
132
+ color-scheme: dark;
133
+ }`;
134
+ }
@@ -6,13 +6,8 @@
6
6
  */
7
7
  export function Reports() {
8
8
  return (
9
- <section className="panel reports-panel" data-reports-view>
10
- <div className="section-header">
11
- <div>
12
- <h3>Hosted reports</h3>
13
- <p className="section-note">Reports published by agent sessions Studio started.</p>
14
- </div>
15
- </div>
9
+ <section className="view-section" data-reports-view aria-label="Hosted reports">
10
+ <p className="view-summary">Reports published by agent sessions Studio started.</p>
16
11
  <div className="reports-layout">
17
12
  <div className="reports-side">
18
13
  <ul id="reports-list" className="reports-list" aria-label="Reports" />
@@ -18,69 +18,50 @@ export function Skills({ skills }: SkillsProps) {
18
18
  const total = inventory.claude.length + inventory.opencode.length + inventory.agents.length;
19
19
 
20
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>
21
+ <section className="view-section" aria-label="Agent skills" data-skills-root>
22
+ <div className="view-intro">
23
+ <label className="view-choice">
24
+ <span>Agent</span>
25
+ <select data-skills-select>
26
+ {PROFILES.map((profile, index) => (
27
+ <option key={profile.id} value={profile.id} selected={index === 0}>
28
+ {profile.label} – {inventory[profile.id].length} skills
29
+ </option>
30
+ ))}
31
+ </select>
32
+ </label>
33
+ <p className="view-summary">
34
+ {total} {total === 1 ? "skill" : "skills"} · installed in this Companion Repository
35
+ </p>
30
36
  </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>
37
+ <div className="skills-panels">
38
+ {PROFILES.map((profile, index) => {
39
+ const active = index === 0;
40
+ const names = inventory[profile.id];
41
+ return (
42
+ <div
43
+ key={profile.id}
44
+ id={`skills-panel-${profile.id}`}
45
+ aria-hidden={!active}
46
+ data-skill-panel={profile.id}
47
+ data-active={active}
48
+ >
49
+ <p className="skills-panel-description">{profile.description}</p>
50
+ {names.length === 0 ? (
51
+ <p className="empty">No {profile.label} skills in this companion.</p>
52
+ ) : (
53
+ <div className="skills-grid">
54
+ {names.map((name) => (
55
+ <div key={name} className="skill-card">
56
+ <code>{name}</code>
57
+ <span>/{name}</span>
58
+ </div>
59
+ ))}
60
+ </div>
61
+ )}
62
+ </div>
63
+ );
64
+ })}
84
65
  </div>
85
66
  </section>
86
67
  );
@@ -96,27 +96,19 @@ function SpecsByArea({ specs, actions }: { specs: StudioSpec[]; actions: SpecAct
96
96
  const groups = groupSpecsByArea(specs);
97
97
 
98
98
  return (
99
- <section className="panel">
100
- <div className="section-header">
101
- <div>
102
- <h3>Specs by Area</h3>
103
- <p className="section-note">Capabilities grouped by the Areas they serve.</p>
104
- </div>
105
- <span className="section-count">
106
- {specs.length} {specs.length === 1 ? "spec" : "specs"}
107
- </span>
108
- </div>
99
+ <section className="view-section" aria-label="Specs by Area">
100
+ <p className="view-summary">
101
+ {specs.length} {specs.length === 1 ? "spec" : "specs"} · capabilities grouped by the Areas
102
+ they serve
103
+ </p>
109
104
  {groups.length === 0 ? (
110
105
  <p className="empty">No specs in this companion.</p>
111
106
  ) : (
112
107
  <div className="specs-area-grid">
113
- {groups.map(([area, areaSpecs], index) => (
108
+ {groups.map(([area, areaSpecs]) => (
114
109
  <section key={area} className="specs-area-card" aria-label={`Area ${area}`}>
115
110
  <div className="specs-area-card-head">
116
- <div className="specs-area-name">
117
- <span className="specs-area-number">{String(index + 1).padStart(2, "0")}</span>
118
- <h4>{area}</h4>
119
- </div>
111
+ <h4 className="specs-area-name">{area}</h4>
120
112
  <span className="specs-area-count">{`${areaSpecs.length} specs`}</span>
121
113
  </div>
122
114
  <div className="specs-card-list">
@@ -0,0 +1,51 @@
1
+ import { copy } from "./page";
2
+
3
+ const AGENT_ORDER = ["claude", "opencode"];
4
+
5
+ /** Copies whatever a control carries and confirms it in the page. */
6
+ export function wireCopyControls(): void {
7
+ document.querySelectorAll("[data-copy]").forEach((node) => {
8
+ node.addEventListener("click", () => {
9
+ const text = node.getAttribute("data-copy");
10
+ if (text) void copy(text, node.getAttribute("data-copy-label") || "prompt");
11
+ });
12
+ });
13
+ }
14
+
15
+ function copyPromptFor(node: Element): string | null {
16
+ const viewed = document.documentElement.getAttribute("data-agent-viewed");
17
+ const order = viewed ? [viewed, ...AGENT_ORDER] : AGENT_ORDER;
18
+ for (const agent of order) {
19
+ const text = node.getAttribute(`data-prompt-${agent}`);
20
+ if (text) return text;
21
+ }
22
+ return null;
23
+ }
24
+
25
+ /** Delegated: a spec row's controls need no per-node wiring. */
26
+ export function wireDelegatedActions(): void {
27
+ if (typeof document.addEventListener !== "function") return;
28
+ document.addEventListener("click", (event) => {
29
+ const target = event.target as Element | null;
30
+ if (!target || typeof target.closest !== "function") return;
31
+ const run = target.closest("[data-studio-action]");
32
+ if (run) {
33
+ const agents = (run.getAttribute("data-studio-agents") || "").split(" ").filter(Boolean);
34
+ document.dispatchEvent(
35
+ new CustomEvent("studio:terminal-action", {
36
+ detail: {
37
+ action: run.getAttribute("data-studio-action"),
38
+ subject: run.getAttribute("data-studio-subject"),
39
+ agents,
40
+ },
41
+ }),
42
+ );
43
+ return;
44
+ }
45
+ const prompt = target.closest("[data-studio-copy-prompt]");
46
+ if (prompt) {
47
+ const text = copyPromptFor(prompt);
48
+ if (text) void copy(text, "prompt");
49
+ }
50
+ });
51
+ }
@@ -0,0 +1,13 @@
1
+ import { COMPANION_STORAGE_KEY } from "./keys";
2
+
3
+ /** Remembers the companion the served page resolved, for the next visit's restore. */
4
+ export function rememberCompanion(): void {
5
+ const scope = document.querySelector("[data-companion]");
6
+ const current = scope && scope.getAttribute("data-companion");
7
+ if (!current) return;
8
+ try {
9
+ localStorage.setItem(COMPANION_STORAGE_KEY, current);
10
+ } catch {
11
+ /* a blocked web store only costs the next visit, never the page */
12
+ }
13
+ }
@@ -0,0 +1,32 @@
1
+ import { wireCopyControls, wireDelegatedActions } from "./actions";
2
+ import { rememberCompanion } from "./companion";
3
+ import { wireNavigation } from "./page";
4
+ import { wireReports } from "./reports";
5
+ import { wireSkillsSelect, wireWorkflowSchemaSelect, wireWorkflowStageSelection } from "./panels";
6
+ import { wireTheme, wireThemeToggle } from "./theme";
7
+ import { wireUnloadGuard } from "./vault-editor";
8
+ import { wireVaultPane } from "./vault-pane";
9
+ import { wireVaultBrowser } from "./vault-tree";
10
+
11
+ /**
12
+ * The only browser code Studio ships beyond the prepaint script: cycling the
13
+ * appearance, remembering the companion on screen, switching the workflow
14
+ * schema, marking the selected workflow stage, copying a prompt, the vault's editor, filter and tree toggle, swapping
15
+ * in a deferred vault tree, and hosted reports. Each needs an API the server
16
+ * does not have; all markup comes from the server.
17
+ */
18
+ export function startStudioClient(): void {
19
+ wireTheme();
20
+ rememberCompanion();
21
+ wireWorkflowSchemaSelect();
22
+ wireWorkflowStageSelection();
23
+ wireSkillsSelect();
24
+ wireThemeToggle();
25
+ wireCopyControls();
26
+ wireDelegatedActions();
27
+ wireUnloadGuard();
28
+ wireNavigation();
29
+ wireVaultPane();
30
+ wireVaultBrowser();
31
+ wireReports();
32
+ }
@@ -0,0 +1,3 @@
1
+ export const THEME_STORAGE_KEY = "mate-studio-theme";
2
+ export const COMPANION_STORAGE_KEY = "mate-studio-companion";
3
+ export const VAULT_TREE_STORAGE_KEY = "mate-studio-vault-tree";
@@ -0,0 +1,3 @@
1
+ import { startStudioClient } from "./index";
2
+
3
+ startStudioClient();
@@ -0,0 +1,146 @@
1
+ let toastTimer: ReturnType<typeof setTimeout> | undefined;
2
+
3
+ export function announce(message: string): void {
4
+ const toast = document.getElementById("studio-toast");
5
+ if (!toast) return;
6
+ toast.textContent = message;
7
+ toast.setAttribute("data-shown", "true");
8
+ clearTimeout(toastTimer);
9
+ toastTimer = setTimeout(() => {
10
+ toast.setAttribute("data-shown", "false");
11
+ }, 1400);
12
+ }
13
+
14
+ export function copy(text: string, label: string): Promise<void> {
15
+ if (
16
+ typeof navigator === "undefined" ||
17
+ !navigator.clipboard ||
18
+ typeof navigator.clipboard.writeText !== "function"
19
+ ) {
20
+ announce("copying is blocked in this browser");
21
+ return Promise.resolve();
22
+ }
23
+ return Promise.resolve()
24
+ .then(() => navigator.clipboard.writeText(text))
25
+ .then(
26
+ () => {
27
+ announce(`copied ${label}`);
28
+ },
29
+ () => {
30
+ /* a blocked clipboard costs the copy, never the page */
31
+ announce("copying is blocked in this browser");
32
+ },
33
+ );
34
+ }
35
+
36
+ export function failure(error: unknown): string {
37
+ const message = (error as { message?: string } | null | undefined)?.message;
38
+ return message ? message : String(error);
39
+ }
40
+
41
+ export function fetchMarkup(url: string, signal?: AbortSignal): Promise<string> {
42
+ return fetch(url, { headers: { accept: "text/html" }, signal }).then((response) => {
43
+ if (!response.ok) throw new Error(`status ${response.status}`);
44
+ return response.text();
45
+ });
46
+ }
47
+
48
+ /** Moves server-rendered nodes into a container; builds no markup of its own. */
49
+ export function moveInto(container: Element, markup: string): void {
50
+ const parsed = new DOMParser().parseFromString(markup, "text/html");
51
+ container.replaceChildren(
52
+ ...Array.from(parsed.body.childNodes, (child) => document.importNode(child, true)),
53
+ );
54
+ }
55
+
56
+ /** The save in flight, so navigation waits for it and a second save is refused. */
57
+ let savePromise: Promise<boolean> | null = null;
58
+
59
+ export function pendingSave(): Promise<boolean> | null {
60
+ return savePromise;
61
+ }
62
+
63
+ export function setPendingSave(save: Promise<boolean> | null): void {
64
+ savePromise = save;
65
+ }
66
+
67
+ export function editor(): HTMLTextAreaElement | null {
68
+ return document.getElementById("vault-editor") as HTMLTextAreaElement | null;
69
+ }
70
+
71
+ export function dirty(): boolean {
72
+ const node = editor();
73
+ return !!node && node.value !== (node.getAttribute("data-vault-original") || "");
74
+ }
75
+
76
+ function askBeforeNavigation(): boolean {
77
+ return !dirty() || window.confirm("Discard unsaved changes?");
78
+ }
79
+
80
+ /**
81
+ * The one check every departure from the shown state goes through: a save in flight settles
82
+ * first, then unsaved text is confirmed. Resolves true when the departure may proceed.
83
+ */
84
+ export function guard(): Promise<boolean> {
85
+ return (savePromise ?? Promise.resolve(true)).then((allowed) => allowed && askBeforeNavigation());
86
+ }
87
+
88
+ /** Set by the vault: shows `url` without replacing the document, or returns false to leave it to the browser. */
89
+ let showInPlace: ((url: string) => boolean) | null = null;
90
+
91
+ export function setInPlaceNavigation(show: ((url: string) => boolean) | null): void {
92
+ showInPlace = show;
93
+ }
94
+
95
+ /**
96
+ * Leaves for `url` once any save has settled, unless the viewer keeps unsaved changes.
97
+ * `inPlace` is for a control that opens a file of the vault and may leave the document alone.
98
+ */
99
+ export function navigate(url: string, inPlace = false): void {
100
+ void guard().then((allowed) => {
101
+ if (!allowed) return;
102
+ if (inPlace && showInPlace && showInPlace(url)) return;
103
+ location.assign(url);
104
+ });
105
+ }
106
+
107
+ /** True for a click the browser would treat as an ordinary same-tab follow of a link. */
108
+ export function isPlainClick(event: MouseEvent): boolean {
109
+ return !(
110
+ event.defaultPrevented ||
111
+ event.button !== 0 ||
112
+ event.metaKey ||
113
+ event.ctrlKey ||
114
+ event.shiftKey ||
115
+ event.altKey
116
+ );
117
+ }
118
+
119
+ function navigationUrl(form: HTMLFormElement, submitter?: HTMLElement | null): string {
120
+ const target = new URL(form.action || location.href, location.href);
121
+ new FormData(form).forEach((value, key) => {
122
+ if (typeof value === "string") target.searchParams.append(key, value);
123
+ });
124
+ const named = submitter as HTMLButtonElement | null | undefined;
125
+ if (named && named.name && named.value) target.searchParams.set(named.name, named.value);
126
+ return target.toString();
127
+ }
128
+
129
+ export function wireNavigation(scope?: ParentNode): void {
130
+ (scope || document)
131
+ .querySelectorAll<HTMLFormElement>('form[method="get"]:not([data-vault-refresh])')
132
+ .forEach((form) => {
133
+ form.addEventListener("submit", (event) => {
134
+ event.preventDefault();
135
+ navigate(
136
+ navigationUrl(form, (event as SubmitEvent).submitter),
137
+ form.hasAttribute("data-vault-open"),
138
+ );
139
+ });
140
+ form.querySelectorAll("select").forEach((select) => {
141
+ select.addEventListener("change", () => {
142
+ navigate(navigationUrl(form));
143
+ });
144
+ });
145
+ });
146
+ }
@@ -0,0 +1,57 @@
1
+ interface SelectPanels {
2
+ root: string;
3
+ select: string;
4
+ panel: string;
5
+ /** Skills panels also hide from assistive technology when inactive. */
6
+ hidePanels: boolean;
7
+ }
8
+
9
+ /** Shows the panel named by a select's value; the server marks the first one active. */
10
+ function wireSelectPanels({ root, select, panel, hidePanels }: SelectPanels): void {
11
+ document.querySelectorAll(`[${root}]`).forEach((scope) => {
12
+ const control = scope.querySelector(`[${select}]`) as HTMLSelectElement | null;
13
+ if (!control) return;
14
+ const panels = scope.querySelectorAll(`[${panel}]`);
15
+ function show(): void {
16
+ panels.forEach((node) => {
17
+ const active = node.getAttribute(panel) === control!.value;
18
+ node.setAttribute("data-active", active ? "true" : "false");
19
+ if (hidePanels) node.setAttribute("aria-hidden", active ? "false" : "true");
20
+ });
21
+ }
22
+ control.addEventListener("change", show);
23
+ show();
24
+ });
25
+ }
26
+
27
+ export function wireWorkflowSchemaSelect(): void {
28
+ wireSelectPanels({
29
+ root: "data-workflow-schema-root",
30
+ select: "data-workflow-schema-select",
31
+ panel: "data-workflow-schema-panel",
32
+ hidePanels: false,
33
+ });
34
+ }
35
+
36
+ export function wireSkillsSelect(): void {
37
+ wireSelectPanels({
38
+ root: "data-skills-root",
39
+ select: "data-skills-select",
40
+ panel: "data-skill-panel",
41
+ hidePanels: true,
42
+ });
43
+ }
44
+
45
+ function markSelectedStage(): void {
46
+ document.querySelectorAll("a[data-map-stage]").forEach((link) => {
47
+ if (link.getAttribute("href") === location.hash) link.setAttribute("aria-current", "true");
48
+ else link.removeAttribute("aria-current");
49
+ });
50
+ }
51
+
52
+ /** Marks the graph node whose card the address targets; CSS shows the card itself. */
53
+ export function wireWorkflowStageSelection(): void {
54
+ if (!document.querySelector("a[data-map-stage]")) return;
55
+ window.addEventListener("hashchange", markSelectedStage);
56
+ markSelectedStage();
57
+ }