@uniqbit/mate-core 0.15.5 → 0.16.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 (183) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +2 -2
  2. package/claude-plugin/hooks/hooks.json +3 -6
  3. package/claude-plugin/hooks/session-guidance.mjs +8 -0
  4. package/claude-plugin/hooks/ts-loader.mjs +19 -0
  5. package/package.json +6 -4
  6. package/src/cli/commands/artifact/artifact.ts +19 -3
  7. package/src/cli/commands/artifact/finish/command.ts +183 -57
  8. package/src/cli/commands/artifact/finish/engine.ts +80 -91
  9. package/src/cli/commands/artifact/finish/finisher.ts +26 -33
  10. package/src/cli/commands/artifact/finish/git.ts +34 -23
  11. package/src/cli/commands/artifact/finish/index.ts +9 -3
  12. package/src/cli/commands/artifact/finish/openspec.ts +225 -97
  13. package/src/cli/commands/artifact/pending/command.ts +175 -0
  14. package/src/cli/commands/artifact/pending/discovery.ts +249 -0
  15. package/src/cli/commands/artifact/pending/index.ts +17 -0
  16. package/src/cli/commands/cap/index-cmd.ts +9 -1
  17. package/src/cli/commands/cap/index.ts +2 -6
  18. package/src/cli/commands/companion/companion.ts +5 -1
  19. package/src/cli/commands/companion/link.ts +2 -2
  20. package/src/cli/commands/companion/sync.ts +92 -0
  21. package/src/cli/commands/doctor.ts +0 -3
  22. package/src/cli/commands/launch/shared.ts +23 -5
  23. package/src/cli/commands/report/collector.ts +72 -78
  24. package/src/cli/commands/report/contract.ts +40 -1
  25. package/src/cli/commands/report/highlight.ts +27 -0
  26. package/src/cli/commands/report/index.ts +11 -18
  27. package/src/cli/commands/report/renderer.ts +199 -2
  28. package/src/cli/commands/report/types.ts +26 -1
  29. package/src/cli/commands/shared/companion-selection.ts +107 -10
  30. package/src/cli/commands/studio/areas.ts +68 -0
  31. package/src/cli/commands/studio/index.ts +69 -0
  32. package/src/cli/commands/studio/inventory.ts +55 -0
  33. package/src/cli/commands/studio/mate-inventory.ts +43 -0
  34. package/src/cli/commands/studio/openspec-cli.ts +198 -0
  35. package/src/cli/commands/studio/payload.ts +184 -0
  36. package/src/cli/commands/studio/routes.ts +2 -0
  37. package/src/cli/commands/studio/selection.ts +61 -0
  38. package/src/cli/commands/studio/server.ts +201 -0
  39. package/src/cli/commands/studio/snapshot.ts +63 -0
  40. package/src/cli/commands/studio/topology.ts +199 -0
  41. package/src/cli/commands/studio/views/client.ts +197 -0
  42. package/src/cli/commands/studio/views/companion-picker.tsx +91 -0
  43. package/src/cli/commands/studio/views/companion-selector.tsx +90 -0
  44. package/src/cli/commands/studio/views/dashboard/changes.tsx +107 -0
  45. package/src/cli/commands/studio/views/dashboard/index.tsx +19 -0
  46. package/src/cli/commands/studio/views/document.tsx +256 -0
  47. package/src/cli/commands/studio/views/error.tsx +20 -0
  48. package/src/cli/commands/studio/views/model.ts +34 -0
  49. package/src/cli/commands/studio/views/pairings.tsx +38 -0
  50. package/src/cli/commands/studio/views/skills/index.tsx +87 -0
  51. package/src/cli/commands/studio/views/specs/index.tsx +101 -0
  52. package/src/cli/commands/studio/views/styles.ts +389 -0
  53. package/src/cli/commands/studio/views/warnings.tsx +21 -0
  54. package/src/cli/commands/studio/views/workflow/index.tsx +21 -0
  55. package/src/cli/commands/studio/views/workflow/steps.ts +329 -0
  56. package/src/cli/commands/studio/views/workflow/transcript.tsx +190 -0
  57. package/src/cli/commands/unwrap.ts +70 -0
  58. package/src/cli/commands/wrap.ts +164 -0
  59. package/src/cli/main.ts +68 -19
  60. package/src/cli/parse-flags.ts +36 -11
  61. package/src/cli/usage.ts +12 -3
  62. package/src/framework.ts +1 -7
  63. package/src/hooks/session-banner.ts +64 -11
  64. package/src/hooks/session-guidance.ts +40 -0
  65. package/src/hooks/validate-artifact-path.ts +108 -35
  66. package/src/lib/fs-utils.ts +9 -0
  67. package/src/lib/install.ts +33 -0
  68. package/src/lib/orchestrator/adapters/base.ts +14 -125
  69. package/src/lib/orchestrator/adapters/claude.ts +0 -11
  70. package/src/lib/orchestrator/adapters/opencode.ts +2 -32
  71. package/src/lib/orchestrator/companion-git-sync.ts +94 -84
  72. package/src/lib/orchestrator/config-store.ts +2 -21
  73. package/src/lib/orchestrator/editor.ts +12 -22
  74. package/src/lib/orchestrator/framework-context.ts +17 -6
  75. package/src/lib/orchestrator/global-config-store.ts +1 -1
  76. package/src/lib/orchestrator/launcher.ts +97 -7
  77. package/src/lib/orchestrator/opencode-guidance.ts +4 -56
  78. package/src/lib/orchestrator/projection-claude-entry.ts +198 -0
  79. package/src/lib/orchestrator/projection-claude-skills.ts +120 -0
  80. package/src/lib/orchestrator/projection-companion-link.ts +62 -0
  81. package/src/lib/orchestrator/projection-entries.ts +377 -0
  82. package/src/lib/orchestrator/projection-record.ts +56 -0
  83. package/src/lib/orchestrator/projection-runtime-documents.ts +424 -0
  84. package/src/lib/orchestrator/projection-types.ts +169 -0
  85. package/src/lib/orchestrator/repo-local-registry.ts +37 -133
  86. package/src/lib/orchestrator/repo-local-store.ts +96 -0
  87. package/src/lib/orchestrator/setup-compatibilities.ts +1 -9
  88. package/src/lib/orchestrator/types.ts +1 -0
  89. package/src/lib/orchestrator/working-repo-projection.ts +366 -0
  90. package/src/lib/orchestrator/workspace-inventory.ts +1 -1
  91. package/src/lib/package-paths.ts +11 -1
  92. package/src/opencode/companion-hooks.ts +89 -245
  93. package/src/opencode/companion-policy.ts +35 -10
  94. package/src/opencode/index.ts +1 -0
  95. package/src/opencode/projected-guidance.ts +56 -0
  96. package/src/opencode/tui.tsx +13 -4
  97. package/src/playbooks/companion-guidance.ts +32 -116
  98. package/src/plugins.ts +0 -1
  99. package/src/runtime/companion-git-state.ts +156 -0
  100. package/src/runtime/companion-git.ts +203 -0
  101. package/src/runtime/companion-guidance.ts +222 -0
  102. package/src/runtime/companion-sync.ts +298 -0
  103. package/src/runtime/env-names.ts +30 -0
  104. package/src/runtime/env.ts +67 -35
  105. package/src/runtime/framework.ts +10 -0
  106. package/src/runtime/freshness.ts +58 -0
  107. package/src/runtime/index.ts +104 -0
  108. package/src/runtime/install.ts +30 -0
  109. package/src/runtime/policy.ts +66 -0
  110. package/src/runtime/projected-guidance.ts +45 -0
  111. package/src/runtime/projection.ts +224 -0
  112. package/src/runtime/repo-local.ts +64 -0
  113. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +56 -0
  114. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +48 -0
  115. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/tasks.md +22 -0
  116. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +31 -90
  117. package/src/templates/capabilities/openspec-cap/mate-v1/templates/design.md +3 -0
  118. package/src/templates/capabilities/openspec-cap/mate-v1/templates/explore-brief.md +6 -6
  119. package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
  120. package/src/templates/capabilities/openspec-cap/mate-v1/templates/tasks.md +3 -0
  121. package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +30 -0
  122. package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +2 -2
  123. package/src/templates/mate-skills/agents/mate-artifact-publish/SKILL.md +185 -0
  124. package/src/templates/mate-skills/agents/mate-artifact-publish/references/openspec.md +227 -0
  125. package/src/templates/mate-skills/agents/mate-domain-modeling/SKILL.md +68 -0
  126. package/src/templates/mate-skills/agents/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
  127. package/src/templates/mate-skills/agents/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
  128. package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +14 -0
  129. package/src/templates/mate-skills/agents/mate-grill-with-docs/SKILL.md +29 -0
  130. package/src/templates/mate-skills/agents/mate-grilling/SKILL.md +49 -0
  131. package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +147 -0
  132. package/src/templates/{capabilities/openspec-cap/mate-skills → mate-skills}/agents/mate-openspec-backfill/SKILL.md +4 -2
  133. package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +139 -0
  134. package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +503 -0
  135. package/src/templates/mate-skills/claude/mate-artifact-publish/SKILL.md +185 -0
  136. package/src/templates/mate-skills/claude/mate-artifact-publish/references/openspec.md +227 -0
  137. package/src/templates/mate-skills/claude/mate-domain-modeling/SKILL.md +68 -0
  138. package/src/templates/mate-skills/claude/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
  139. package/src/templates/mate-skills/claude/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
  140. package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +14 -0
  141. package/src/templates/mate-skills/claude/mate-grill-with-docs/SKILL.md +29 -0
  142. package/src/templates/mate-skills/claude/mate-grilling/SKILL.md +49 -0
  143. package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +147 -0
  144. package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +67 -0
  145. package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +139 -0
  146. package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +503 -0
  147. package/src/templates/report-assets/README.md +32 -0
  148. package/src/templates/report-assets/mermaid.LICENSE +21 -0
  149. package/src/templates/report-assets/mermaid.min.js +4376 -0
  150. package/src/templates/root/TEMPLATE_CLAUDE.md +1 -9
  151. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +1476 -197
  152. package/src/tools/setup/capabilities/graphify.ts +16 -7
  153. package/src/tools/setup/capabilities/openspec.ts +63 -56
  154. package/src/tools/setup/capabilities/tokensave.ts +47 -0
  155. package/src/tools/setup/engine.ts +34 -6
  156. package/src/tools/setup/mate.ts +42 -13
  157. package/src/tools/setup/plugin.ts +9 -0
  158. package/src/tools/setup/plugins/guidance.ts +11 -1
  159. package/src/tools/setup/providers/claude-format.ts +49 -4
  160. package/src/tools/setup/providers/claude-plugin-hooks.ts +117 -0
  161. package/src/tools/setup/providers/claude.ts +55 -220
  162. package/src/tools/setup/providers/opencode.ts +41 -14
  163. package/src/tools/setup/runtime-documents.ts +174 -0
  164. package/src/tools/setup/surface-target.ts +50 -0
  165. package/src/tools/setup/working-repo-cleanup.ts +33 -26
  166. package/src/tools/setup/working-repo-local-state.ts +21 -1
  167. package/src/tools/setup.ts +25 -3
  168. package/wrappers/bin/graphify +57 -8
  169. package/wrappers/bin/openspec +50 -3
  170. package/claude-plugin/hooks/artifact-finish-nudge.mjs +0 -8
  171. package/src/cli/commands/cap/headroom.ts +0 -52
  172. package/src/cli/commands/workspace/list.ts +0 -25
  173. package/src/cli/commands/workspace/materialize.ts +0 -46
  174. package/src/cli/commands/workspace/workspace.ts +0 -22
  175. package/src/hooks/artifact-finish-nudge.ts +0 -244
  176. package/src/lib/orchestrator/headroom/proxy.ts +0 -116
  177. package/src/lib/orchestrator/workspace-materialize.ts +0 -80
  178. package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/SKILL.md +0 -51
  179. package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/references/openspec.md +0 -134
  180. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +0 -58
  181. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +0 -139
  182. package/src/tools/setup/capabilities/headroom.ts +0 -57
  183. /package/src/cli/commands/{workspace → companion}/open.ts +0 -0
@@ -0,0 +1,199 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { parse } from "yaml";
5
+
6
+ import { readOpenSpecProjectSchema } from "../cap/openspec";
7
+ import {
8
+ readAllChangeStatus,
9
+ readOpenSpecJson,
10
+ WORKFLOW_CAPABILITY_ID,
11
+ type OpenSpecResult,
12
+ } from "./openspec-cli";
13
+
14
+ export interface WorkflowArtifact {
15
+ id: string;
16
+ description?: string;
17
+ generates?: string;
18
+ requires: string[];
19
+ }
20
+
21
+ export interface WorkflowEdge {
22
+ from: string;
23
+ to: string;
24
+ }
25
+
26
+ export interface WorkflowApplyStep {
27
+ requires: string[];
28
+ tracks?: string;
29
+ }
30
+
31
+ export interface WorkflowTopology {
32
+ schemaName: string;
33
+ /** Capability that owns this workflow, so a consumer can tell which skills drive it. */
34
+ workflowCapabilityId: string;
35
+ schemaVersion?: number;
36
+ description?: string;
37
+ source: "project" | "package";
38
+ artifacts: WorkflowArtifact[];
39
+ edges: WorkflowEdge[];
40
+ apply: WorkflowApplyStep;
41
+ }
42
+
43
+ export interface SchemaLocation {
44
+ path: string;
45
+ }
46
+
47
+ export interface WorkflowTopologyDeps {
48
+ readProjectSchemaName?: (companionPath: string) => string | undefined;
49
+ defaultSchemaName?: (companionPath: string) => Promise<string | undefined>;
50
+ locateSchema?: (
51
+ companionPath: string,
52
+ schemaName: string,
53
+ ) => Promise<OpenSpecResult<SchemaLocation>>;
54
+ readFile?: (filePath: string) => Promise<string>;
55
+ }
56
+
57
+ const FALLBACK_SCHEMA_NAME = "spec-driven";
58
+
59
+ async function readDefaultSchemaName(companionPath: string): Promise<string | undefined> {
60
+ const status = await readAllChangeStatus(companionPath);
61
+ if (!status.ok) return undefined;
62
+ return status.value.changes?.find((change) => change.planningHome?.defaultSchema)?.planningHome
63
+ ?.defaultSchema;
64
+ }
65
+
66
+ function locateSchemaViaCli(
67
+ companionPath: string,
68
+ schemaName: string,
69
+ ): Promise<OpenSpecResult<SchemaLocation>> {
70
+ return readOpenSpecJson(companionPath, ["schema", "which", schemaName, "--json"]);
71
+ }
72
+
73
+ function toStringArray(value: unknown): string[] {
74
+ return Array.isArray(value)
75
+ ? value.filter((entry): entry is string => typeof entry === "string")
76
+ : [];
77
+ }
78
+
79
+ function parseTopology(
80
+ content: string,
81
+ fallbackName: string,
82
+ source: WorkflowTopology["source"],
83
+ ): WorkflowTopology | { reason: string } {
84
+ let parsed: unknown;
85
+ try {
86
+ parsed = parse(content);
87
+ } catch (error) {
88
+ return { reason: error instanceof Error ? error.message : String(error) };
89
+ }
90
+ if (!parsed || typeof parsed !== "object") return { reason: "schema is not a mapping" };
91
+
92
+ const document = parsed as Record<string, unknown>;
93
+ if (!Array.isArray(document.artifacts)) {
94
+ return { reason: "schema declares no artifacts list" };
95
+ }
96
+
97
+ const artifacts: WorkflowArtifact[] = [];
98
+ for (const entry of document.artifacts) {
99
+ if (!entry || typeof entry !== "object") continue;
100
+ const artifact = entry as Record<string, unknown>;
101
+ if (typeof artifact.id !== "string") continue;
102
+ artifacts.push({
103
+ id: artifact.id,
104
+ ...(typeof artifact.description === "string" ? { description: artifact.description } : {}),
105
+ ...(typeof artifact.generates === "string" ? { generates: artifact.generates } : {}),
106
+ requires: toStringArray(artifact.requires),
107
+ });
108
+ }
109
+
110
+ const declared = new Set(artifacts.map((artifact) => artifact.id));
111
+ const edges: WorkflowEdge[] = [];
112
+ for (const artifact of artifacts) {
113
+ for (const requirement of artifact.requires) {
114
+ if (declared.has(requirement)) edges.push({ from: requirement, to: artifact.id });
115
+ }
116
+ }
117
+
118
+ const applyBlock =
119
+ document.apply && typeof document.apply === "object"
120
+ ? (document.apply as Record<string, unknown>)
121
+ : {};
122
+
123
+ return {
124
+ schemaName: typeof document.name === "string" ? document.name : fallbackName,
125
+ workflowCapabilityId: WORKFLOW_CAPABILITY_ID,
126
+ ...(typeof document.version === "number" ? { schemaVersion: document.version } : {}),
127
+ ...(typeof document.description === "string" ? { description: document.description } : {}),
128
+ source,
129
+ artifacts,
130
+ edges,
131
+ apply: {
132
+ requires: toStringArray(applyBlock.requires),
133
+ ...(typeof applyBlock.tracks === "string" ? { tracks: applyBlock.tracks } : {}),
134
+ },
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Reads the workflow graph of whichever schema a Companion Repository
140
+ * resolves. Nothing about a particular schema is assumed: artifacts, their
141
+ * dependency edges, and the apply step all come from the schema document, so a
142
+ * schema that gains an artifact renders without a code change.
143
+ */
144
+ export async function readWorkflowTopology(
145
+ companionPath: string,
146
+ deps: WorkflowTopologyDeps = {},
147
+ ): Promise<OpenSpecResult<WorkflowTopology>> {
148
+ const readSchemaName = deps.readProjectSchemaName ?? readOpenSpecProjectSchema;
149
+ const readDefaultName = deps.defaultSchemaName ?? readDefaultSchemaName;
150
+ const locate = deps.locateSchema ?? locateSchemaViaCli;
151
+ const readFile = deps.readFile ?? ((filePath: string) => fs.readFile(filePath, "utf8"));
152
+
153
+ const schemaName =
154
+ readSchemaName(companionPath) ?? (await readDefaultName(companionPath)) ?? FALLBACK_SCHEMA_NAME;
155
+ const command = `workflow topology for ${schemaName}`;
156
+
157
+ let content: string | undefined;
158
+ let source: WorkflowTopology["source"] = "project";
159
+ try {
160
+ content = await readFile(
161
+ path.join(companionPath, "openspec", "schemas", schemaName, "schema.yaml"),
162
+ );
163
+ } catch {
164
+ /** No project-local schema: fall through to the CLI-resolved location. */
165
+ }
166
+
167
+ if (content === undefined) {
168
+ const located = await locate(companionPath, schemaName);
169
+ if (!located.ok) {
170
+ return {
171
+ ok: false,
172
+ failure: {
173
+ command,
174
+ reason: `could not locate schema ${schemaName}: ${located.failure.reason}`,
175
+ },
176
+ };
177
+ }
178
+ try {
179
+ content = await readFile(path.join(located.value.path, "schema.yaml"));
180
+ source = "package";
181
+ } catch (error) {
182
+ return {
183
+ ok: false,
184
+ failure: {
185
+ command,
186
+ reason: `could not read schema ${schemaName}: ${
187
+ error instanceof Error ? error.message : String(error)
188
+ }`,
189
+ },
190
+ };
191
+ }
192
+ }
193
+
194
+ const topology = parseTopology(content, schemaName, source);
195
+ if ("reason" in topology) {
196
+ return { ok: false, failure: { command, reason: `schema ${schemaName}: ${topology.reason}` } };
197
+ }
198
+ return { ok: true, value: topology };
199
+ }
@@ -0,0 +1,197 @@
1
+ import { COMPANION_DIGEST_PATTERN, COMPANION_PARAM } from "../selection";
2
+
3
+ export const THEME_STORAGE_KEY = "mate-studio-theme";
4
+ export const COMPANION_STORAGE_KEY = "mate-studio-companion";
5
+
6
+ /**
7
+ * Runs in `<head>` before first paint. A server-rendered document arrives with
8
+ * its markup already in place, so a remembered appearance applied after load
9
+ * would paint the other one first and then repaint, and a remembered companion
10
+ * restored after load would paint the picker before leaving it.
11
+ *
12
+ * The restore names the companion in the URL and reloads, because the URL is
13
+ * what the server answers. A companion parameter that is present and empty is
14
+ * a deliberate "no companion" and is left alone, so the redirect cannot loop
15
+ * and the picker stays reachable.
16
+ */
17
+ export const STUDIO_PREPAINT_SCRIPT = `(function () {
18
+ try {
19
+ var chosen = localStorage.getItem(${JSON.stringify(THEME_STORAGE_KEY)});
20
+ if (chosen === "dark" || chosen === "light") {
21
+ document.documentElement.setAttribute("data-theme", chosen);
22
+ }
23
+ } catch (error) {
24
+ /* a blocked web store only costs the preference, never the page */
25
+ }
26
+
27
+ try {
28
+ var params = new URLSearchParams(location.search);
29
+ var last = localStorage.getItem(${JSON.stringify(COMPANION_STORAGE_KEY)});
30
+ if (!params.has(${JSON.stringify(COMPANION_PARAM)}) && ${String(COMPANION_DIGEST_PATTERN)}.test(last || "")) {
31
+ params.set(${JSON.stringify(COMPANION_PARAM)}, last);
32
+ location.replace(location.pathname + "?" + params.toString());
33
+ }
34
+ } catch (error) {
35
+ /* a blocked web store only costs the restore, never the page */
36
+ }
37
+ })();`;
38
+
39
+ /**
40
+ * The only browser code Studio ships: cycling the appearance, remembering the
41
+ * companion on screen, switching workflow schema tabs, and copying a prompt.
42
+ * Each needs an API the server does not have, and nothing else here renders,
43
+ * fetches, or holds state.
44
+ */
45
+ export const STUDIO_CLIENT_SCRIPT = `(function () {
46
+ var THEME_KEY = ${JSON.stringify(THEME_STORAGE_KEY)};
47
+ var COMPANION_KEY = ${JSON.stringify(COMPANION_STORAGE_KEY)};
48
+ var THEME_CYCLE = ["system", "dark", "light"];
49
+ var toastTimer;
50
+
51
+ function readTheme() {
52
+ try {
53
+ var stored = localStorage.getItem(THEME_KEY);
54
+ return THEME_CYCLE.indexOf(stored) === -1 ? "system" : stored;
55
+ } catch (error) {
56
+ return "system";
57
+ }
58
+ }
59
+
60
+ function storeTheme(theme) {
61
+ try {
62
+ localStorage.setItem(THEME_KEY, theme);
63
+ } catch (error) {
64
+ /* a blocked web store only costs the preference, never the page */
65
+ }
66
+ }
67
+
68
+ var theme = readTheme();
69
+
70
+ function applyTheme(next) {
71
+ theme = next;
72
+ var root = document.documentElement;
73
+ if (next === "system") root.removeAttribute("data-theme");
74
+ else root.setAttribute("data-theme", next);
75
+ var control = document.getElementById("studio-theme");
76
+ if (control) {
77
+ control.textContent = "Theme: " + next;
78
+ control.setAttribute("data-theme-state", next);
79
+ }
80
+ }
81
+
82
+ function announce(message) {
83
+ var toast = document.getElementById("studio-toast");
84
+ if (!toast) return;
85
+ toast.textContent = message;
86
+ toast.setAttribute("data-shown", "true");
87
+ clearTimeout(toastTimer);
88
+ toastTimer = setTimeout(function () {
89
+ toast.setAttribute("data-shown", "false");
90
+ }, 1400);
91
+ }
92
+
93
+ function copy(text, label) {
94
+ return navigator.clipboard.writeText(text).then(
95
+ function () {
96
+ announce("copied " + label);
97
+ },
98
+ function () {
99
+ /* a blocked clipboard costs the copy, never the page */
100
+ announce("copying is blocked in this browser");
101
+ },
102
+ );
103
+ }
104
+
105
+ function rememberCompanion() {
106
+ var scope = document.querySelector("[data-companion]");
107
+ var current = scope && scope.getAttribute("data-companion");
108
+ if (!current) return;
109
+ try {
110
+ localStorage.setItem(COMPANION_KEY, current);
111
+ } catch (error) {
112
+ /* a blocked web store only costs the next visit, never the page */
113
+ }
114
+ }
115
+
116
+ function wireWorkflowSchemaTabs() {
117
+ document.querySelectorAll("[data-workflow-schema-root]").forEach(function (root) {
118
+ var tabs = root.querySelectorAll("[data-workflow-schema-profile]");
119
+ var panels = root.querySelectorAll("[data-workflow-schema-panel]");
120
+
121
+ function select(profile) {
122
+ tabs.forEach(function (tab) {
123
+ tab.setAttribute(
124
+ "aria-selected",
125
+ tab.getAttribute("data-workflow-schema-profile") === profile ? "true" : "false",
126
+ );
127
+ });
128
+ panels.forEach(function (panel) {
129
+ panel.setAttribute(
130
+ "data-active",
131
+ panel.getAttribute("data-workflow-schema-panel") === profile ? "true" : "false",
132
+ );
133
+ });
134
+ }
135
+
136
+ tabs.forEach(function (tab) {
137
+ tab.addEventListener("click", function () {
138
+ select(tab.getAttribute("data-workflow-schema-profile"));
139
+ });
140
+ });
141
+
142
+ var selected = root.querySelector('[aria-selected="true"]');
143
+ if (selected) select(selected.getAttribute("data-workflow-schema-profile"));
144
+ });
145
+ }
146
+
147
+ function wireSkillTabs() {
148
+ document.querySelectorAll("[data-skills-root]").forEach(function (root) {
149
+ var tabs = root.querySelectorAll("[data-skill-profile]");
150
+ var panels = root.querySelectorAll("[data-skill-panel]");
151
+
152
+ function select(profile) {
153
+ tabs.forEach(function (tab) {
154
+ tab.setAttribute(
155
+ "aria-selected",
156
+ tab.getAttribute("data-skill-profile") === profile ? "true" : "false",
157
+ );
158
+ });
159
+ panels.forEach(function (panel) {
160
+ var active = panel.getAttribute("data-skill-panel") === profile;
161
+ panel.setAttribute("data-active", active ? "true" : "false");
162
+ panel.setAttribute("aria-hidden", active ? "false" : "true");
163
+ });
164
+ }
165
+
166
+ tabs.forEach(function (tab) {
167
+ tab.addEventListener("click", function () {
168
+ select(tab.getAttribute("data-skill-profile"));
169
+ });
170
+ });
171
+
172
+ var selected = root.querySelector('[aria-selected="true"]');
173
+ if (selected) select(selected.getAttribute("data-skill-profile"));
174
+ });
175
+ }
176
+
177
+ applyTheme(theme);
178
+ rememberCompanion();
179
+ wireWorkflowSchemaTabs();
180
+ wireSkillTabs();
181
+
182
+ var toggle = document.getElementById("studio-theme");
183
+ if (toggle) {
184
+ toggle.addEventListener("click", function () {
185
+ var next = THEME_CYCLE[(THEME_CYCLE.indexOf(theme) + 1) % THEME_CYCLE.length];
186
+ applyTheme(next);
187
+ storeTheme(next);
188
+ });
189
+ }
190
+
191
+ document.querySelectorAll("[data-copy]").forEach(function (node) {
192
+ node.addEventListener("click", function () {
193
+ var text = node.getAttribute("data-copy");
194
+ if (text) copy(text, node.getAttribute("data-copy-label") || "prompt");
195
+ });
196
+ });
197
+ })();`;
@@ -0,0 +1,91 @@
1
+ /** @jsxImportSource hono/jsx */
2
+
3
+ import type { StudioInventory, StudioInventoryCompanion } from "../inventory";
4
+ import { companionDigest, type StudioSelection } from "../selection";
5
+ import { healthNote } from "./model";
6
+
7
+ interface CompanionPickerProps {
8
+ inventory: StudioInventory;
9
+ selection: StudioSelection;
10
+ }
11
+
12
+ /** Kept short so a card leads with what distinguishes it, not the shared prefix. */
13
+ const PAIRING_CHIP_LIMIT = 3;
14
+
15
+ function companionName(companionPath: string): string {
16
+ const segments = companionPath.split(/[\\/]+/).filter(Boolean);
17
+ return segments[segments.length - 1] ?? companionPath;
18
+ }
19
+
20
+ /**
21
+ * The unselected main content. One submit button per companion, so the first
22
+ * choice is a click on the thing being chosen rather than a trip through the
23
+ * sidebar dropdown — the same GET navigation either way.
24
+ */
25
+ export function CompanionPicker({ inventory, selection }: CompanionPickerProps) {
26
+ const companions = inventory.companions;
27
+
28
+ if (companions.length === 0) {
29
+ return (
30
+ <section className="panel">
31
+ <h3>No Companion Repository</h3>
32
+ <p className="empty">No Companion Repository is registered on this machine.</p>
33
+ <p className="muted">
34
+ Run <span className="mono">mate companion setup</span> in a Working Repository to register
35
+ one.
36
+ </p>
37
+ </section>
38
+ );
39
+ }
40
+
41
+ return (
42
+ <section className="panel">
43
+ <h3>Choose a Companion Repository</h3>
44
+ <p className="note">
45
+ Studio serves one companion at a time, and remembers the one you choose for your next visit.
46
+ </p>
47
+ <form method="get" action="/">
48
+ {selection.view === "dashboard" ? null : (
49
+ <input type="hidden" name="view" value={selection.view} />
50
+ )}
51
+ <div className="picker-grid">
52
+ {companions.map((companion) => (
53
+ <CompanionCard key={companionDigest(companion.path)} companion={companion} />
54
+ ))}
55
+ </div>
56
+ </form>
57
+ </section>
58
+ );
59
+ }
60
+
61
+ function CompanionCard({ companion }: { companion: StudioInventoryCompanion }) {
62
+ const note = healthNote(companion);
63
+ const pairings = companion.pairings.slice(0, PAIRING_CHIP_LIMIT);
64
+ const hidden = companion.pairings.length - pairings.length;
65
+
66
+ return (
67
+ <button
68
+ type="submit"
69
+ name="companion"
70
+ value={companionDigest(companion.path)}
71
+ className="picker-card"
72
+ data-unready={companion.health === "ready" ? undefined : "true"}
73
+ >
74
+ <span className="picker-card-name">{companionName(companion.path)}</span>
75
+ <span className="picker-card-path mono">{companion.path}</span>
76
+ <span className="picker-card-meta">
77
+ {note ? <span className="chip chip-invalid">{note}</span> : null}
78
+ {companion.pairings.length === 0 ? (
79
+ <span className="chip">no linked working repository</span>
80
+ ) : (
81
+ pairings.map((pairing) => (
82
+ <span key={pairing.repositoryPath} className="chip">
83
+ {pairing.repositoryId}
84
+ </span>
85
+ ))
86
+ )}
87
+ {hidden > 0 ? <span className="chip">+{hidden} more</span> : null}
88
+ </span>
89
+ </button>
90
+ );
91
+ }
@@ -0,0 +1,90 @@
1
+ /** @jsxImportSource hono/jsx */
2
+
3
+ import type { StudioInventoryCompanion } from "../inventory";
4
+ import { companionDigest, resolveCompanion, type StudioSelection } from "../selection";
5
+ import { healthNote, type StudioPage } from "./model";
6
+
7
+ interface CompanionSelectorProps {
8
+ inventory: StudioPage["inventory"];
9
+ selection: StudioSelection;
10
+ }
11
+
12
+ /**
13
+ * A GET form, so choosing a companion is a navigation to the URL naming it. The
14
+ * change is deliberately not carried over: it belongs to the companion being
15
+ * left behind.
16
+ */
17
+ export function CompanionSelector({ inventory, selection }: CompanionSelectorProps) {
18
+ const companions = inventory.companions;
19
+ const selected = resolveCompanion(inventory, selection.companionDigest);
20
+
21
+ return (
22
+ <form className="sidebar-scope" method="get" action="/">
23
+ <span className="sidebar-label">Scope</span>
24
+ {selection.view === "dashboard" ? null : (
25
+ <input type="hidden" name="view" value={selection.view} />
26
+ )}
27
+ {/* eslint-disable-next-line react/no-unknown-property -- raw HTML attribute: hono/jsx server-renders this inline handler */}
28
+ <select name="companion" aria-label="Companion Repository" onchange="this.form.submit()">
29
+ <PlaceholderOption companionCount={companions.length} selected={selected} />
30
+ {companions.map((companion) => {
31
+ const digest = companionDigest(companion.path);
32
+ const note = healthNote(companion);
33
+ const unready = companion.health !== "ready";
34
+ return (
35
+ <option
36
+ key={digest}
37
+ value={digest}
38
+ selected={digest === selection.companionDigest}
39
+ className={unready ? "unready" : undefined}
40
+ data-unready={unready ? "true" : undefined}
41
+ >
42
+ {note ? `${companion.path} (${note})` : companion.path}
43
+ </option>
44
+ );
45
+ })}
46
+ </select>
47
+ <SelectorNote companionCount={companions.length} selected={selected} />
48
+ </form>
49
+ );
50
+ }
51
+
52
+ interface EmptyStateProps {
53
+ companionCount: number;
54
+ selected: StudioInventoryCompanion | null;
55
+ }
56
+
57
+ /**
58
+ * Without it the browser shows the first companion as if it were chosen, which
59
+ * a null selection is not. Disabled so it cannot be chosen back, and dropped
60
+ * once a companion resolves.
61
+ */
62
+ function PlaceholderOption({ companionCount, selected }: EmptyStateProps) {
63
+ if (companionCount === 0) {
64
+ return (
65
+ <option value="" className="placeholder">
66
+ no companion registered
67
+ </option>
68
+ );
69
+ }
70
+ if (selected) return null;
71
+ return (
72
+ <option value="" selected disabled className="placeholder">
73
+ select a companion…
74
+ </option>
75
+ );
76
+ }
77
+
78
+ function SelectorNote({ companionCount, selected }: EmptyStateProps) {
79
+ if (!selected) {
80
+ return (
81
+ <p className="note">
82
+ {companionCount === 0
83
+ ? "No Companion Repository is registered on this machine."
84
+ : "Select a Companion Repository to see its state."}
85
+ </p>
86
+ );
87
+ }
88
+ const note = healthNote(selected);
89
+ return note ? <p className="note unready">{note}</p> : null;
90
+ }
@@ -0,0 +1,107 @@
1
+ /** @jsxImportSource hono/jsx */
2
+
3
+ import type { StudioChange } from "../../payload";
4
+
5
+ interface ChangesProps {
6
+ changes: StudioChange[];
7
+ }
8
+
9
+ function progressLabel(change: StudioChange): string {
10
+ const total = change.totalTasks ?? 0;
11
+ return total === 0 ? "no tasks" : `${change.completedTasks ?? 0} / ${total}`;
12
+ }
13
+
14
+ function progressPercent(change: StudioChange): number {
15
+ const total = change.totalTasks ?? 0;
16
+ if (total === 0) return 0;
17
+ return Math.min(100, Math.round(((change.completedTasks ?? 0) / total) * 100));
18
+ }
19
+
20
+ export function Changes({ changes }: ChangesProps) {
21
+ return (
22
+ <section className="panel lookup-panel">
23
+ <div className="section-header">
24
+ <div>
25
+ <h3>Changes</h3>
26
+ <p className="section-note">Active work tracked by this Companion Repository.</p>
27
+ </div>
28
+ <span className="section-count">
29
+ {changes.length} {changes.length === 1 ? "change" : "changes"}
30
+ </span>
31
+ </div>
32
+ {changes.length === 0 ? (
33
+ <p className="empty">No changes in this companion.</p>
34
+ ) : (
35
+ <div className="scroll">
36
+ <table className="lookup-table">
37
+ <thead>
38
+ <tr>
39
+ <th scope="col">Change</th>
40
+ <th scope="col">Status</th>
41
+ <th scope="col">Progress</th>
42
+ <th scope="col">Artifacts</th>
43
+ </tr>
44
+ </thead>
45
+ <tbody>
46
+ {changes.map((change) => (
47
+ <tr key={change.name}>
48
+ <td data-label="Change">
49
+ <div className="change-cell">
50
+ <span className="mono">{change.name}</span>
51
+ {change.valid === false ? (
52
+ <span className="chip chip-invalid">
53
+ {`${change.issueCount ?? 0} issues`}
54
+ </span>
55
+ ) : null}
56
+ </div>
57
+ </td>
58
+ <td data-label="Status">
59
+ <span className="status-chip" data-status={change.status ?? "unknown"}>
60
+ {change.status ?? "unknown"}
61
+ </span>
62
+ </td>
63
+ <td className="numeric" data-label="Progress">
64
+ <span className="progress-label">{progressLabel(change)}</span>
65
+ {(change.totalTasks ?? 0) === 0 ? null : (
66
+ <span
67
+ className="bar"
68
+ role="progressbar"
69
+ aria-valuemin="0"
70
+ aria-valuemax="100"
71
+ aria-valuenow={progressPercent(change)}
72
+ aria-label={`${progressLabel(change)} tasks complete`}
73
+ >
74
+ <span
75
+ className="bar-fill"
76
+ style={{ width: `${progressPercent(change)}%` }}
77
+ />
78
+ </span>
79
+ )}
80
+ </td>
81
+ <td data-label="Artifacts">
82
+ {change.artifacts.length === 0 ? (
83
+ <span className="muted">—</span>
84
+ ) : (
85
+ <div className="artifact-list">
86
+ {change.artifacts.map((artifact) => (
87
+ <span
88
+ key={artifact.id}
89
+ className={
90
+ artifact.status === "done" ? "chip chip-done" : "chip chip-pending"
91
+ }
92
+ >
93
+ {artifact.id}
94
+ </span>
95
+ ))}
96
+ </div>
97
+ )}
98
+ </td>
99
+ </tr>
100
+ ))}
101
+ </tbody>
102
+ </table>
103
+ </div>
104
+ )}
105
+ </section>
106
+ );
107
+ }