@uniqbit/mate-core 0.17.0-canary.9 → 0.17.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/package.json +11 -5
- package/src/cli/commands/artifact/finish/index.ts +0 -9
- package/src/cli/commands/artifact/finish/openspec.ts +2 -2
- package/src/cli/commands/artifact/finish/registry.ts +1 -1
- package/src/cli/commands/artifact/pending/discovery.ts +1 -1
- package/src/cli/commands/artifact/pending/index.ts +0 -16
- package/src/cli/commands/cap/graphify.ts +1 -1
- package/src/cli/commands/cap/index-cmd.ts +1 -1
- package/src/cli/commands/cap/tokensave.ts +4 -3
- package/src/cli/commands/companion/companion.ts +4 -0
- package/src/cli/commands/companion/hub.ts +2 -1
- package/src/cli/commands/companion/list.ts +2 -1
- package/src/cli/commands/companion/prepare.ts +2 -2
- package/src/cli/commands/companion/update.ts +334 -0
- package/src/cli/commands/doctor.ts +2 -1
- package/src/cli/commands/install.ts +59 -5
- package/src/cli/commands/launch/shared.ts +2 -1
- package/src/cli/commands/plugin/plugin.ts +4 -0
- package/src/cli/commands/plugin/verify.ts +25 -0
- package/src/cli/commands/report/highlight.ts +1 -4
- package/src/cli/commands/report/renderer.ts +0 -2
- package/src/cli/commands/studio/access.ts +201 -0
- package/src/cli/commands/studio/index.ts +207 -26
- package/src/cli/commands/studio/openspec-cli.ts +1 -1
- package/src/cli/commands/studio/selection.ts +127 -11
- package/src/cli/commands/studio/server.ts +355 -64
- package/src/cli/commands/studio/terminal-assets.ts +50 -0
- package/src/cli/commands/studio/terminal-launch.ts +71 -0
- package/src/cli/commands/studio/terminal.ts +593 -0
- package/src/cli/commands/studio/vault.ts +320 -75
- package/src/cli/commands/studio/views/client.ts +90 -9
- package/src/cli/commands/studio/views/companion-picker.tsx +9 -5
- package/src/cli/commands/studio/views/companion-selector.tsx +11 -7
- package/src/cli/commands/studio/views/document.tsx +411 -107
- package/src/cli/commands/studio/views/model.ts +14 -1
- package/src/cli/commands/studio/views/selection-fields.tsx +14 -0
- package/src/cli/commands/studio/views/styles.ts +106 -13
- package/src/cli/commands/studio/views/terminal.tsx +426 -0
- package/src/cli/commands/studio/views/vault/state.ts +42 -0
- package/src/cli/commands/studio/views/workflow/steps.ts +10 -3
- package/src/cli/commands/update.ts +11 -8
- package/src/cli/companion-link-wizard.tsx +1 -1
- package/src/cli/companion-selector.tsx +1 -1
- package/src/cli/launch-selector.tsx +1 -1
- package/src/cli/main.ts +2 -1
- package/src/cli/usage.ts +7 -3
- package/src/cli/write-json-stdout.ts +2 -2
- package/src/lib/components/spinner.tsx +2 -2
- package/src/lib/components/startup-progress.tsx +1 -1
- package/src/lib/context-mode-package.ts +1 -1
- package/src/lib/opencode-plugin-package.ts +3 -0
- package/src/lib/orchestrator/adapters/opencode.ts +53 -13
- package/src/lib/orchestrator/companion-hub.ts +3 -3
- package/src/lib/orchestrator/companion-registry-store.ts +1 -1
- package/src/lib/orchestrator/config-store.ts +2 -2
- package/src/lib/orchestrator/editor.ts +4 -12
- package/src/lib/orchestrator/engine-guard.ts +9 -1
- package/src/lib/orchestrator/projection-entries.ts +1 -1
- package/src/lib/orchestrator/projection-record.ts +1 -1
- package/src/lib/orchestrator/projection-types.ts +0 -4
- package/src/lib/orchestrator/repo-local-registry.ts +0 -1
- package/src/lib/orchestrator/root-context.ts +1 -1
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
- package/src/lib/orchestrator/setup-preflight.ts +1 -1
- package/src/lib/orchestrator/types.ts +1 -1
- package/src/lib/orchestrator/working-repo-projection.ts +1 -1
- package/src/lib/orchestrator/workspace-inventory.ts +2 -2
- package/src/lib/package-paths.ts +1 -11
- package/src/lib/package-root.ts +43 -0
- package/src/lib/prebuilt-workspace.ts +1 -1
- package/src/lib/public-npm.ts +1 -1
- package/src/opencode/companion-hooks.ts +131 -153
- package/src/opencode/index.ts +12 -6
- package/src/opencode/tui.tsx +46 -49
- package/src/playbooks/companion-guidance.ts +1 -28
- package/src/runtime/companion-sync.ts +0 -3
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +2 -1
- package/src/templates/providers/opencode/opencode.json +6 -4
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +500 -408
- package/src/tools/setup/capabilities/context-mode.ts +45 -25
- package/src/tools/setup/capabilities/graphify-shared.ts +3 -3
- package/src/tools/setup/capabilities/graphify.ts +2 -2
- package/src/tools/setup/capabilities/openspec.ts +2 -7
- package/src/tools/setup/capabilities/react-doctor.ts +1 -1
- package/src/tools/setup/capabilities/rtk.ts +2 -2
- package/src/tools/setup/capabilities/tokensave.ts +98 -12
- package/src/tools/setup/context-services.ts +3 -6
- package/src/tools/setup/dynamic-plugins/declarations.ts +47 -0
- package/src/tools/setup/dynamic-plugins/frozen.ts +215 -0
- package/src/tools/setup/dynamic-plugins/hydrate.ts +1 -40
- package/src/tools/setup/dynamic-plugins/loader.ts +16 -0
- package/src/tools/setup/dynamic-plugins/policy.ts +55 -0
- package/src/tools/setup/dynamic-plugins/staging.ts +53 -0
- package/src/tools/setup/dynamic-plugins/verify.ts +70 -0
- package/src/tools/setup/mate.ts +1 -1
- package/src/tools/setup/providers/claude-format.ts +1 -1
- package/src/tools/setup/providers/claude.ts +1 -1
- package/src/tools/setup/providers/opencode-format.ts +220 -25
- package/src/tools/setup/providers/opencode.ts +63 -21
- package/src/tools/setup/runtime-documents.ts +9 -11
- package/src/tools/setup.ts +0 -4
- package/src/cli/commands/studio/views/pairings.tsx +0 -38
- package/src/templates/providers/opencode/tui.json +0 -3
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
|
|
4
|
-
import type {
|
|
5
|
+
import type { Cleanup, Context } from "@opencode/plugin/promise/plugin";
|
|
5
6
|
|
|
6
7
|
import {
|
|
7
8
|
companionForkRefusal,
|
|
@@ -15,7 +16,6 @@ import {
|
|
|
15
16
|
extractPatchPaths,
|
|
16
17
|
isArtifactPath,
|
|
17
18
|
normalizeTargetPath,
|
|
18
|
-
readContext,
|
|
19
19
|
shouldBlockArtifactWrite,
|
|
20
20
|
type CompanionContext,
|
|
21
21
|
} from "./companion-policy";
|
|
@@ -43,67 +43,80 @@ const REACT_DOCTOR_ARGS = [
|
|
|
43
43
|
"30",
|
|
44
44
|
];
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
46
|
+
export type CommandResult = { exitCode: number | null; output: string };
|
|
47
|
+
export type CommandRunner = (
|
|
48
|
+
command: string,
|
|
49
|
+
args: string[],
|
|
50
|
+
cwd: string,
|
|
51
|
+
) => Promise<CommandResult>;
|
|
52
|
+
type SessionPrompt = Pick<Context["session"], "prompt">;
|
|
53
|
+
|
|
54
|
+
/** Combined stdout and stderr; rejects only when the process cannot be spawned. */
|
|
55
|
+
export const runCommand: CommandRunner = (command, args, cwd) =>
|
|
56
|
+
new Promise((resolve, reject) => {
|
|
57
|
+
const child = spawn(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
58
|
+
let output = "";
|
|
59
|
+
child.stdout.setEncoding("utf8").on("data", (chunk: string) => (output += chunk));
|
|
60
|
+
child.stderr.setEncoding("utf8").on("data", (chunk: string) => (output += chunk));
|
|
61
|
+
child.once("error", reject);
|
|
62
|
+
child.once("close", (exitCode) => resolve({ exitCode, output: output.trim() }));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
function reactDoctorCommand(repo: string): { command: string; args: string[] } {
|
|
66
|
+
const localBin = path.join(repo, "node_modules", ".bin", "react-doctor");
|
|
67
|
+
const mateBin = process.env.MATE_REACT_DOCTOR_BIN_PATH;
|
|
68
|
+
const doctorBin = mateBin && fs.existsSync(mateBin) ? mateBin : localBin;
|
|
69
|
+
return fs.existsSync(doctorBin)
|
|
70
|
+
? { command: doctorBin, args: REACT_DOCTOR_ARGS }
|
|
71
|
+
: {
|
|
72
|
+
command: "npx",
|
|
73
|
+
args: ["--yes", `react-doctor@${REACT_DOCTOR_VERSION}`, ...REACT_DOCTOR_ARGS],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Tracks edited sessions and scans each at most once per idle, never twice
|
|
79
|
+
* concurrently for the same session.
|
|
80
|
+
*/
|
|
81
|
+
export function createReactDoctorScanner(
|
|
82
|
+
context: CompanionContext,
|
|
83
|
+
session: SessionPrompt,
|
|
84
|
+
run: CommandRunner = runCommand,
|
|
85
|
+
) {
|
|
86
|
+
const dirtySessions = new Set<string>();
|
|
87
|
+
const scansInFlight = new Set<string>();
|
|
72
88
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
} finally {
|
|
105
|
-
scansInFlight.delete(sessionID);
|
|
106
|
-
}
|
|
89
|
+
return {
|
|
90
|
+
markEdited(tool: string, sessionID: string): void {
|
|
91
|
+
if (REACT_DOCTOR_EDIT_TOOLS.has(tool)) dirtySessions.add(sessionID);
|
|
92
|
+
},
|
|
93
|
+
async idle(sessionID: string): Promise<void> {
|
|
94
|
+
const repo = context.repositoryPath;
|
|
95
|
+
if (!repo || scansInFlight.has(sessionID) || !dirtySessions.delete(sessionID)) return;
|
|
96
|
+
scansInFlight.add(sessionID);
|
|
97
|
+
try {
|
|
98
|
+
const { command, args } = reactDoctorCommand(repo);
|
|
99
|
+
const { exitCode, output } = await run(command, args, repo);
|
|
100
|
+
if (exitCode === 0 || !output) return;
|
|
101
|
+
if (REACT_DOCTOR_NON_LINT_FAILURES.some((re) => re.test(output))) return;
|
|
102
|
+
|
|
103
|
+
await session
|
|
104
|
+
.prompt({
|
|
105
|
+
sessionID,
|
|
106
|
+
text:
|
|
107
|
+
"React Doctor found issues in the changed files. Review this output and fix " +
|
|
108
|
+
"the regressions before finishing. For confirmed issues that cannot be fixed " +
|
|
109
|
+
"now, create GitHub issues with the rule, file/line, confidence, impact, and " +
|
|
110
|
+
`proposed fix.\n\n${output}`,
|
|
111
|
+
} as never)
|
|
112
|
+
.catch(() => {});
|
|
113
|
+
} catch {
|
|
114
|
+
/** Never break the session because a diagnostic scan failed. */
|
|
115
|
+
} finally {
|
|
116
|
+
scansInFlight.delete(sessionID);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
/**
|
|
@@ -120,7 +133,6 @@ const repairedCompanions = new Set<string>();
|
|
|
120
133
|
*/
|
|
121
134
|
export async function repairCompanionGitOnce(
|
|
122
135
|
context: CompanionContext,
|
|
123
|
-
client: PluginInput["client"] | undefined,
|
|
124
136
|
env: Record<string, string | undefined> = process.env,
|
|
125
137
|
): Promise<string[]> {
|
|
126
138
|
if (
|
|
@@ -132,27 +144,12 @@ export async function repairCompanionGitOnce(
|
|
|
132
144
|
if (repairedCompanions.has(context.companionPath)) return [];
|
|
133
145
|
repairedCompanions.add(context.companionPath);
|
|
134
146
|
|
|
135
|
-
const notes = unattendedSyncStalenessLines(syncCompanionUnattended(context.companionPath));
|
|
136
|
-
if (notes.length === 0) return [];
|
|
137
|
-
|
|
138
147
|
/**
|
|
139
148
|
* Operator-facing only: a model told to run the command would run it
|
|
140
|
-
* unattended.
|
|
141
|
-
*
|
|
149
|
+
* unattended. The V2 server API has no toast; the notes reach the operator
|
|
150
|
+
* through the persisted record and the TUI's staleness lines.
|
|
142
151
|
*/
|
|
143
|
-
|
|
144
|
-
await client?.tui?.showToast({
|
|
145
|
-
query: { directory: context.repositoryPath },
|
|
146
|
-
body: {
|
|
147
|
-
title: `${context.frameworkName} companion`,
|
|
148
|
-
message: notes.join("\n"),
|
|
149
|
-
variant: "warning",
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
} catch {
|
|
153
|
-
/** The note survives in the persisted record and the TUI's staleness lines. */
|
|
154
|
-
}
|
|
155
|
-
return notes;
|
|
152
|
+
return unattendedSyncStalenessLines(syncCompanionUnattended(context.companionPath));
|
|
156
153
|
}
|
|
157
154
|
|
|
158
155
|
/** Test seam: the once-per-session guard is process-wide by design. */
|
|
@@ -180,83 +177,64 @@ export function refuseForkedCompanionWrite(
|
|
|
180
177
|
if (refusal) throw new Error(refusal);
|
|
181
178
|
}
|
|
182
179
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
180
|
+
/** Throws when the tool call would write an artifact outside the companion. */
|
|
181
|
+
export function guardToolInput(context: CompanionContext, tool: string, input: unknown): void {
|
|
182
|
+
const args = (input ?? {}) as Record<string, unknown>;
|
|
183
|
+
const filePaths =
|
|
184
|
+
tool === "write" || tool === "edit"
|
|
185
|
+
? [String(args.filePath ?? "")]
|
|
186
|
+
: tool === "apply_patch"
|
|
187
|
+
? extractPatchPaths(String(args.patchText ?? ""))
|
|
188
|
+
: [];
|
|
189
|
+
|
|
190
|
+
for (const filePath of filePaths) {
|
|
191
|
+
if (context.repositoryPath && filePath && shouldBlockArtifactWrite(context, filePath)) {
|
|
192
|
+
throw new Error(buildArtifactError(context, filePath));
|
|
193
|
+
}
|
|
194
|
+
refuseForkedCompanionWrite(context, filePath);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
187
197
|
|
|
188
|
-
export
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
198
|
+
export type CompanionHooksOptions = {
|
|
199
|
+
/** Test seam for the React Doctor process. */
|
|
200
|
+
run?: CommandRunner;
|
|
201
|
+
};
|
|
192
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Registers the companion guardrails on an OpenCode V2 plugin context.
|
|
205
|
+
* The caller resolves `context` and skips this when no companion resolved.
|
|
206
|
+
*/
|
|
207
|
+
export async function registerCompanionHooks(
|
|
208
|
+
api: Context,
|
|
209
|
+
context: CompanionContext,
|
|
210
|
+
options: CompanionHooksOptions = {},
|
|
211
|
+
): Promise<Cleanup | undefined> {
|
|
193
212
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* Doctor scan — to save a synchronization that is optional by design.
|
|
213
|
+
* Anything escaping the repair would cost the session every guardrail
|
|
214
|
+
* below to save a synchronization that is optional by design.
|
|
197
215
|
*/
|
|
198
|
-
await repairCompanionGitOnce(context
|
|
216
|
+
await repairCompanionGitOnce(context).catch(() => []);
|
|
199
217
|
|
|
200
|
-
|
|
201
|
-
|
|
218
|
+
await api.tool.hook("execute.before", (event) => {
|
|
219
|
+
guardToolInput(context, event.tool, event.input);
|
|
220
|
+
});
|
|
202
221
|
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (
|
|
210
|
-
!context.reactDoctorEnabled ||
|
|
211
|
-
reactDoctorScansInFlight.has(sessionID) ||
|
|
212
|
-
!dirtyReactDoctorSessions.delete(sessionID)
|
|
213
|
-
) {
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
await runReactDoctorScan(context, client, $, sessionID, reactDoctorScansInFlight);
|
|
217
|
-
},
|
|
218
|
-
}
|
|
219
|
-
: {}),
|
|
220
|
-
"tool.execute.before": async (input: ToolBeforeInput, output: ToolBeforeOutput) => {
|
|
221
|
-
const toolName = String(input.tool ?? "");
|
|
222
|
-
const args = output.args ?? {};
|
|
223
|
-
if (context.repositoryPath && ["write", "edit"].includes(toolName)) {
|
|
224
|
-
const filePath = String(args.filePath ?? "");
|
|
225
|
-
if (filePath && shouldBlockArtifactWrite(context, filePath)) {
|
|
226
|
-
throw new Error(buildArtifactError(context, filePath));
|
|
227
|
-
}
|
|
228
|
-
refuseForkedCompanionWrite(context, filePath);
|
|
229
|
-
}
|
|
230
|
-
if (context.repositoryPath && toolName === "apply_patch") {
|
|
231
|
-
for (const filePath of extractPatchPaths(String(args.patchText ?? ""))) {
|
|
232
|
-
if (shouldBlockArtifactWrite(context, filePath)) {
|
|
233
|
-
throw new Error(buildArtifactError(context, filePath));
|
|
234
|
-
}
|
|
235
|
-
refuseForkedCompanionWrite(context, filePath);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
if (!context.repositoryPath && toolName === "write") {
|
|
239
|
-
refuseForkedCompanionWrite(context, String(args.filePath ?? ""));
|
|
240
|
-
}
|
|
241
|
-
if (!context.repositoryPath && toolName === "edit") {
|
|
242
|
-
refuseForkedCompanionWrite(context, String(args.filePath ?? ""));
|
|
243
|
-
}
|
|
244
|
-
if (!context.repositoryPath && toolName === "apply_patch") {
|
|
245
|
-
for (const filePath of extractPatchPaths(String(args.patchText ?? ""))) {
|
|
246
|
-
refuseForkedCompanionWrite(context, filePath);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
},
|
|
250
|
-
...(context.repositoryPath
|
|
251
|
-
? {
|
|
252
|
-
"tool.execute.after": async (input: ToolAfterInput) => {
|
|
253
|
-
if (context.reactDoctorEnabled && REACT_DOCTOR_EDIT_TOOLS.has(input.tool)) {
|
|
254
|
-
dirtyReactDoctorSessions.add(input.sessionID);
|
|
255
|
-
}
|
|
256
|
-
},
|
|
257
|
-
}
|
|
258
|
-
: {}),
|
|
259
|
-
};
|
|
260
|
-
};
|
|
222
|
+
if (!context.repositoryPath || !context.reactDoctorEnabled) return;
|
|
223
|
+
|
|
224
|
+
const scanner = createReactDoctorScanner(context, api.session, options.run);
|
|
225
|
+
await api.tool.hook("execute.after", (event) => {
|
|
226
|
+
if (event.status === "completed") scanner.markEdited(event.tool, event.sessionID);
|
|
227
|
+
});
|
|
261
228
|
|
|
262
|
-
|
|
229
|
+
const controller = new AbortController();
|
|
230
|
+
void (async () => {
|
|
231
|
+
try {
|
|
232
|
+
for await (const event of api.event.subscribe({ signal: controller.signal })) {
|
|
233
|
+
if (event.type === "session.idle") await scanner.idle(event.data.sessionID);
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
/** Plugin shutdown aborts the subscription; event delivery is best effort. */
|
|
237
|
+
}
|
|
238
|
+
})();
|
|
239
|
+
return () => controller.abort();
|
|
240
|
+
}
|
package/src/opencode/index.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Server-side building blocks for authoring OpenCode plugins: the companion
|
|
3
|
+
* guidance hooks and the companion policy helpers. Session-runtime only —
|
|
4
|
+
* modules here must not import framework internals (see the import-isolation
|
|
5
|
+
* test). Must not reach OpenTUI or Solid: the Mate TUI plugin lives on the
|
|
6
|
+
* `./opencode/tui` subpath.
|
|
6
7
|
*/
|
|
7
|
-
export {
|
|
8
|
-
|
|
8
|
+
export {
|
|
9
|
+
createReactDoctorScanner,
|
|
10
|
+
guardToolInput,
|
|
11
|
+
registerCompanionHooks,
|
|
12
|
+
type CommandRunner,
|
|
13
|
+
type CompanionHooksOptions,
|
|
14
|
+
} from "./companion-hooks";
|
|
9
15
|
export { resolveOpenCodeGuidance, type OpenCodeGuidanceResolution } from "./projected-guidance";
|
|
10
16
|
export * from "./companion-policy";
|
package/src/opencode/tui.tsx
CHANGED
|
@@ -1,82 +1,79 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
/* oxlint-disable react/no-unknown-property, react/react-in-jsx-scope, react/jsx-key, react-doctor/jsx-key */
|
|
3
3
|
/**
|
|
4
|
+
* OpenCode V2 TUI plugin module (`{ id, setup }`), OpenCode 2.x only.
|
|
4
5
|
* Solid JSX, not React: `key` is not a reconciliation prop here and `TextProps`
|
|
5
6
|
* does not accept it, so the jsx-key rules cannot be satisfied — only suppressed.
|
|
6
7
|
* Static arrays render through `.map`; a reactive one would need `<For>`.
|
|
7
8
|
*/
|
|
8
|
-
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui";
|
|
9
|
-
|
|
10
9
|
import { mateVersion } from "../runtime/install";
|
|
11
10
|
import { readContext } from "./companion-policy";
|
|
12
11
|
|
|
12
|
+
/** Minimal structural slice of the OpenCode V2 TUI API; the published SDK ships no V2 TUI types. */
|
|
13
|
+
export type MateTuiApi = {
|
|
14
|
+
/** Host getter: returns the current theme tokens on every read. */
|
|
15
|
+
readonly theme: {
|
|
16
|
+
text: {
|
|
17
|
+
muted: string;
|
|
18
|
+
feedback?: { warning?: { base: string } };
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
ui: {
|
|
22
|
+
slot(claim: MateSlotClaim): () => void;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type MateSlotClaim = {
|
|
27
|
+
prepend?: "home.footer";
|
|
28
|
+
append?: "sidebar.content";
|
|
29
|
+
render: () => unknown;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type MateTuiPluginModule = {
|
|
33
|
+
id: string;
|
|
34
|
+
setup(api: MateTuiApi): void | Promise<void>;
|
|
35
|
+
};
|
|
36
|
+
|
|
13
37
|
const MIDNIGHT_PURPLE_BRIGHT = "#c084fc";
|
|
14
|
-
const NARROW_TERMINAL_WIDTH = 80;
|
|
15
38
|
const MATE_VERSION = process.env.MATE_VERSION ?? mateVersion();
|
|
16
39
|
|
|
17
|
-
function SessionContext({
|
|
18
|
-
api,
|
|
19
|
-
compact = false,
|
|
20
|
-
sidebar = false,
|
|
21
|
-
}: {
|
|
22
|
-
api: TuiPluginApi;
|
|
23
|
-
compact?: boolean;
|
|
24
|
-
sidebar?: boolean;
|
|
25
|
-
}) {
|
|
40
|
+
function SessionContext({ api, sidebar = false }: { api: MateTuiApi; sidebar?: boolean }) {
|
|
26
41
|
const context = readContext();
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
if (compact) {
|
|
30
|
-
return (
|
|
31
|
-
<box width="100%" paddingTop={0} flexShrink={0}>
|
|
32
|
-
<text fg={theme.textMuted}>
|
|
33
|
-
mate v{MATE_VERSION} | repo: {context.repositoryPath} | mate: {context.companionPath}
|
|
34
|
-
</text>
|
|
35
|
-
</box>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
42
|
+
const text = api.theme.text;
|
|
43
|
+
const warning = text.feedback?.warning?.base ?? text.muted;
|
|
38
44
|
|
|
39
45
|
return (
|
|
40
|
-
<box
|
|
46
|
+
<box
|
|
47
|
+
width="100%"
|
|
48
|
+
maxWidth={sidebar ? undefined : 75}
|
|
49
|
+
paddingLeft={sidebar ? 0 : 2}
|
|
50
|
+
paddingRight={sidebar ? 0 : 2}
|
|
51
|
+
paddingBottom={sidebar ? 1 : 0}
|
|
52
|
+
flexShrink={0}
|
|
53
|
+
>
|
|
41
54
|
<text fg={MIDNIGHT_PURPLE_BRIGHT}>mate v{MATE_VERSION}</text>
|
|
42
|
-
<text fg={
|
|
43
|
-
<text fg={
|
|
55
|
+
<text fg={text.muted}>repo: {context.repositoryPath}</text>
|
|
56
|
+
<text fg={text.muted}>mate: {context.companionPath}</text>
|
|
44
57
|
{context.stalenessLines.map((note) => (
|
|
45
|
-
<text fg={
|
|
58
|
+
<text fg={warning}>{note}</text>
|
|
46
59
|
))}
|
|
47
60
|
</box>
|
|
48
61
|
);
|
|
49
62
|
}
|
|
50
63
|
|
|
51
|
-
|
|
64
|
+
function setup(api: MateTuiApi): void {
|
|
52
65
|
const context = readContext();
|
|
53
66
|
if (!context.companionPath || !context.repositoryPath) {
|
|
54
67
|
return;
|
|
55
68
|
}
|
|
56
69
|
|
|
57
|
-
api.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
home_bottom() {
|
|
61
|
-
return <SessionContext api={api} />;
|
|
62
|
-
},
|
|
63
|
-
sidebar_content() {
|
|
64
|
-
return <SessionContext api={api} sidebar />;
|
|
65
|
-
},
|
|
66
|
-
app_bottom() {
|
|
67
|
-
if (api.renderer.width >= NARROW_TERMINAL_WIDTH || api.route.current.name !== "session") {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return <SessionContext api={api} compact />;
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
};
|
|
70
|
+
api.ui.slot({ prepend: "home.footer", render: () => <SessionContext api={api} /> });
|
|
71
|
+
api.ui.slot({ append: "sidebar.content", render: () => <SessionContext api={api} sidebar /> });
|
|
72
|
+
}
|
|
76
73
|
|
|
77
|
-
const plugin:
|
|
74
|
+
const plugin: MateTuiPluginModule = {
|
|
78
75
|
id: "mate-companion-tui",
|
|
79
|
-
|
|
76
|
+
setup,
|
|
80
77
|
};
|
|
81
78
|
|
|
82
79
|
export default plugin;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildCompanionGuidance as buildCompanionGuidanceWith,
|
|
3
|
-
buildCompanionPolicyXml as buildCompanionPolicyXmlWith,
|
|
4
3
|
type GuidanceContext,
|
|
5
4
|
} from "../runtime/companion-guidance";
|
|
6
5
|
import { getWrapperBinPath } from "../lib/package-paths";
|
|
@@ -16,33 +15,7 @@ import { getWrapperBinPath } from "../lib/package-paths";
|
|
|
16
15
|
* passes the projected path instead, and so calls the runtime builders direct.
|
|
17
16
|
*/
|
|
18
17
|
|
|
19
|
-
export {
|
|
20
|
-
GRAPHIFY_SHARED_COMPANION_PATH_CONTRACT,
|
|
21
|
-
GRAPHIFY_COMPANION_PATH_CONTRACT,
|
|
22
|
-
buildCodebaseExplorationGuidanceSection,
|
|
23
|
-
hasGraphifyCapability,
|
|
24
|
-
hasOpenspecCapability,
|
|
25
|
-
hasTokensaveCapability,
|
|
26
|
-
type GuidanceCapability,
|
|
27
|
-
type GuidanceContext,
|
|
28
|
-
} from "../runtime/companion-guidance";
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Build just the `<companion-policy>` XML block: paths, CLI tools, and
|
|
32
|
-
* mandatory rules (including the capability-gated `openspec-publish` rule).
|
|
33
|
-
* Does not include codebase-exploration guidance — see
|
|
34
|
-
* `buildCompanionGuidance` for the merged single-string form, or call
|
|
35
|
-
* `buildCodebaseExplorationGuidanceSection` directly when that guidance is
|
|
36
|
-
* delivered through its own channel (as OpenCode's guidance contract does).
|
|
37
|
-
*/
|
|
38
|
-
export function buildCompanionPolicyXml(
|
|
39
|
-
context: GuidanceContext,
|
|
40
|
-
options: { wrapperBinPath?: string } = {},
|
|
41
|
-
): string {
|
|
42
|
-
return buildCompanionPolicyXmlWith(context, {
|
|
43
|
-
wrapperBinPath: options.wrapperBinPath ?? getWrapperBinPath(),
|
|
44
|
-
});
|
|
45
|
-
}
|
|
18
|
+
export { GRAPHIFY_SHARED_COMPANION_PATH_CONTRACT } from "../runtime/companion-guidance";
|
|
46
19
|
|
|
47
20
|
/**
|
|
48
21
|
* Build the merged single-string guidance: the companion-policy XML plus
|
|
@@ -12,7 +12,6 @@ import fs from "node:fs";
|
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
|
-
companionGitStatePath,
|
|
16
15
|
COMPANION_SYNC_TTL_MS,
|
|
17
16
|
FORK_VERDICT_TTL_MS,
|
|
18
17
|
isCompanionSyncDue,
|
|
@@ -294,5 +293,3 @@ export function forkRefusalMessage(companionPath: string, fork: CompanionForkSta
|
|
|
294
293
|
`Writing artifacts now risks losing work in the reconciliation. Run \`${COMPANION_SYNC_COMMAND}\` first.`,
|
|
295
294
|
].join("\n");
|
|
296
295
|
}
|
|
297
|
-
|
|
298
|
-
export { companionGitStatePath };
|
|
@@ -39,7 +39,8 @@ artifacts:
|
|
|
39
39
|
template: tasks.md
|
|
40
40
|
instruction: |
|
|
41
41
|
Create a flat implementation checklist that satisfies the delta specs. Keep it short: this
|
|
42
|
-
|
|
42
|
+
profile uses the shared `/openspec-explore` and `/openspec-propose` planning steps but
|
|
43
|
+
intentionally writes no explore, proposal, or design artifact.
|
|
43
44
|
|
|
44
45
|
Number tasks `N.M` and group them under `## N. <group>` headers so apply tracking can address them individually.
|
|
45
46
|
Read `openspec/mate-conventions.yaml` before writing the artifact and preserve its scope rules.
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
"$schema": "https://opencode.ai/config.json",
|
|
3
3
|
"compaction": {
|
|
4
4
|
"auto": true,
|
|
5
|
-
"
|
|
6
|
-
"
|
|
5
|
+
"buffer": 20000,
|
|
6
|
+
"keep": {
|
|
7
|
+
"tokens": 15000
|
|
8
|
+
}
|
|
7
9
|
},
|
|
8
10
|
"tool_output": {
|
|
9
|
-
"max_lines":
|
|
10
|
-
"max_bytes":
|
|
11
|
+
"max_lines": 1000,
|
|
12
|
+
"max_bytes": 40000
|
|
11
13
|
},
|
|
12
14
|
"references": {
|
|
13
15
|
"mate": ".."
|