infinicode 2.8.27 → 2.8.28
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/.opencode/plugins/home-logo-animation.tsx +124 -124
- package/.opencode/plugins/mesh-commands.tsx +140 -140
- package/.opencode/plugins/routing-mode-display.tsx +138 -138
- package/.opencode/themes/infinibot-gold.json +222 -222
- package/.opencode/tui.json +8 -8
- package/README.md +623 -623
- package/bin/robopark.js +2 -2
- package/dist/kernel/agents/backends/registry.js +22 -2
- package/dist/kernel/federation/dashboard-html.d.ts +1 -1
- package/dist/kernel/federation/dashboard-html.js +1512 -1403
- package/dist/kernel/plugins/dashboard-plugin.js +8 -8
- package/dist/robopark/add-robot.js +25 -3
- package/dist/robopark/auto-start.js +38 -38
- package/dist/robopark/probe.js +15 -15
- package/dist/robopark/setup-livekit.js +52 -52
- package/package.json +2 -2
- package/packages/robopark/README.md +63 -63
- package/packages/robopark/pi-client/README.md +17 -17
- package/packages/robopark/scheduler/main.py +827 -2
- package/packages/robopark/scheduler/preview_agent.py +7 -0
- package/packages/robopark/scheduler/requirements-robot.txt +9 -9
- package/packages/robopark/vision/README.md +66 -66
- package/packages/robopark/vision/requirements-demo.txt +16 -16
|
@@ -1,124 +1,124 @@
|
|
|
1
|
-
import { createSignal, onCleanup } from "solid-js";
|
|
2
|
-
import { ASCII_VIDEO_FPS, ASCII_VIDEO_FRAMES } from "../../dist/ascii-video-animation.js";
|
|
3
|
-
|
|
4
|
-
// On the home screen the logo slot shares the column with (above it) a flexible
|
|
5
|
-
// spacer + a 4-row gap, and (below it) the prompt box and footer. Those lower
|
|
6
|
-
// elements do not shrink, so we must keep the logo within `terminalHeight minus
|
|
7
|
-
// this reserve` or its top/bottom gets clipped off the screen.
|
|
8
|
-
const VERTICAL_CHROME_ROWS = 9;
|
|
9
|
-
// Fallbacks used only before the renderer reports a real size.
|
|
10
|
-
const FALLBACK_COLUMNS = 120;
|
|
11
|
-
const FALLBACK_ROWS = 40;
|
|
12
|
-
|
|
13
|
-
type BoundingBox = {
|
|
14
|
-
lines: string[];
|
|
15
|
-
left: number;
|
|
16
|
-
width: number;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
function boundingBox(frame: string): BoundingBox | null {
|
|
20
|
-
const lines = frame.split("\n");
|
|
21
|
-
const nonEmptyRows = lines
|
|
22
|
-
.map((line, index) => (line.trim() ? index : -1))
|
|
23
|
-
.filter(index => index >= 0);
|
|
24
|
-
|
|
25
|
-
if (nonEmptyRows.length === 0) return null;
|
|
26
|
-
|
|
27
|
-
const top = nonEmptyRows[0];
|
|
28
|
-
const bottom = nonEmptyRows[nonEmptyRows.length - 1];
|
|
29
|
-
const visibleLines = lines.slice(top, bottom + 1).map(line => line.replace(/\s+$/, ""));
|
|
30
|
-
|
|
31
|
-
const left = Math.min(
|
|
32
|
-
...visibleLines.map(line => (line.length ? line.length - line.trimStart().length : Number.POSITIVE_INFINITY))
|
|
33
|
-
);
|
|
34
|
-
const right = Math.max(...visibleLines.map(line => line.length), 0);
|
|
35
|
-
|
|
36
|
-
return { lines: visibleLines, left: Number.isFinite(left) ? left : 0, width: Math.max(0, right - left) };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Sample `count` evenly spaced indices across a span of `length` items.
|
|
40
|
-
function sampleIndices(length: number, count: number): number[] {
|
|
41
|
-
if (count >= length) return Array.from({ length }, (_, index) => index);
|
|
42
|
-
const indices: number[] = [];
|
|
43
|
-
for (let i = 0; i < count; i++) {
|
|
44
|
-
indices.push(Math.min(length - 1, Math.round((i * (length - 1)) / (count - 1 || 1))));
|
|
45
|
-
}
|
|
46
|
-
return indices;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function fitFrame(frame: string, terminalColumns: number, terminalRows: number): string {
|
|
50
|
-
const box = boundingBox(frame);
|
|
51
|
-
if (!box) return "";
|
|
52
|
-
|
|
53
|
-
const { lines, left, width } = box;
|
|
54
|
-
const height = lines.length;
|
|
55
|
-
|
|
56
|
-
const availableColumns = Math.max(20, terminalColumns - 2);
|
|
57
|
-
const availableRows = Math.max(6, terminalRows - VERTICAL_CHROME_ROWS);
|
|
58
|
-
|
|
59
|
-
// Single uniform scale so the whole logo fits without distorting its aspect.
|
|
60
|
-
const scale = Math.min(1, availableColumns / Math.max(1, width), availableRows / height);
|
|
61
|
-
const targetHeight = Math.max(1, Math.round(height * scale));
|
|
62
|
-
const targetWidth = Math.max(1, Math.round(width * scale));
|
|
63
|
-
|
|
64
|
-
const rowIndices = sampleIndices(height, targetHeight);
|
|
65
|
-
const columnIndices = sampleIndices(width, targetWidth);
|
|
66
|
-
|
|
67
|
-
return rowIndices
|
|
68
|
-
.map(rowIndex => {
|
|
69
|
-
const line = lines[rowIndex];
|
|
70
|
-
return columnIndices
|
|
71
|
-
.map(columnIndex => line[left + columnIndex] ?? " ")
|
|
72
|
-
.join("")
|
|
73
|
-
.replace(/\s+$/, "");
|
|
74
|
-
})
|
|
75
|
-
.join("\n");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function AnimatedLogo(props: { renderer?: any }) {
|
|
79
|
-
const [frameIndex, setFrameIndex] = createSignal(0);
|
|
80
|
-
|
|
81
|
-
// Track the real terminal size from the TUI renderer (process.stdout.rows is
|
|
82
|
-
// unreliable inside the plugin runtime). Re-fit whenever the terminal resizes.
|
|
83
|
-
const renderer = props.renderer;
|
|
84
|
-
const readSize = () => ({
|
|
85
|
-
columns: Math.max(40, renderer?.width ?? process.stdout.columns ?? FALLBACK_COLUMNS),
|
|
86
|
-
rows: Math.max(10, renderer?.height ?? process.stdout.rows ?? FALLBACK_ROWS),
|
|
87
|
-
});
|
|
88
|
-
const [size, setSize] = createSignal(readSize());
|
|
89
|
-
|
|
90
|
-
if (typeof renderer?.on === "function") {
|
|
91
|
-
const onResize = () => setSize(readSize());
|
|
92
|
-
renderer.on("resize", onResize);
|
|
93
|
-
onCleanup(() => {
|
|
94
|
-
if (typeof renderer.off === "function") renderer.off("resize", onResize);
|
|
95
|
-
else if (typeof renderer.removeListener === "function") renderer.removeListener("resize", onResize);
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const frameDelayMs = Math.max(16, Math.round(1000 / ASCII_VIDEO_FPS));
|
|
100
|
-
const timer = setInterval(() => {
|
|
101
|
-
setFrameIndex(index => (index + 1) % ASCII_VIDEO_FRAMES.length);
|
|
102
|
-
}, frameDelayMs);
|
|
103
|
-
onCleanup(() => clearInterval(timer));
|
|
104
|
-
|
|
105
|
-
return (
|
|
106
|
-
<text fg="#fbbf24">
|
|
107
|
-
{fitFrame(ASCII_VIDEO_FRAMES[frameIndex()] ?? "", size().columns, size().rows)}
|
|
108
|
-
</text>
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const tui = async (api: any) => {
|
|
113
|
-
api.slots.register({
|
|
114
|
-
slots: {
|
|
115
|
-
home_logo() {
|
|
116
|
-
return <AnimatedLogo renderer={api.renderer} />;
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
// The TUI plugin loader reads `mod.default` (strict mode) and requires an object
|
|
123
|
-
// with a tui() function — a bare `export const tui` is silently skipped.
|
|
124
|
-
export default { id: "infinicode-home-logo", tui };
|
|
1
|
+
import { createSignal, onCleanup } from "solid-js";
|
|
2
|
+
import { ASCII_VIDEO_FPS, ASCII_VIDEO_FRAMES } from "../../dist/ascii-video-animation.js";
|
|
3
|
+
|
|
4
|
+
// On the home screen the logo slot shares the column with (above it) a flexible
|
|
5
|
+
// spacer + a 4-row gap, and (below it) the prompt box and footer. Those lower
|
|
6
|
+
// elements do not shrink, so we must keep the logo within `terminalHeight minus
|
|
7
|
+
// this reserve` or its top/bottom gets clipped off the screen.
|
|
8
|
+
const VERTICAL_CHROME_ROWS = 9;
|
|
9
|
+
// Fallbacks used only before the renderer reports a real size.
|
|
10
|
+
const FALLBACK_COLUMNS = 120;
|
|
11
|
+
const FALLBACK_ROWS = 40;
|
|
12
|
+
|
|
13
|
+
type BoundingBox = {
|
|
14
|
+
lines: string[];
|
|
15
|
+
left: number;
|
|
16
|
+
width: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function boundingBox(frame: string): BoundingBox | null {
|
|
20
|
+
const lines = frame.split("\n");
|
|
21
|
+
const nonEmptyRows = lines
|
|
22
|
+
.map((line, index) => (line.trim() ? index : -1))
|
|
23
|
+
.filter(index => index >= 0);
|
|
24
|
+
|
|
25
|
+
if (nonEmptyRows.length === 0) return null;
|
|
26
|
+
|
|
27
|
+
const top = nonEmptyRows[0];
|
|
28
|
+
const bottom = nonEmptyRows[nonEmptyRows.length - 1];
|
|
29
|
+
const visibleLines = lines.slice(top, bottom + 1).map(line => line.replace(/\s+$/, ""));
|
|
30
|
+
|
|
31
|
+
const left = Math.min(
|
|
32
|
+
...visibleLines.map(line => (line.length ? line.length - line.trimStart().length : Number.POSITIVE_INFINITY))
|
|
33
|
+
);
|
|
34
|
+
const right = Math.max(...visibleLines.map(line => line.length), 0);
|
|
35
|
+
|
|
36
|
+
return { lines: visibleLines, left: Number.isFinite(left) ? left : 0, width: Math.max(0, right - left) };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Sample `count` evenly spaced indices across a span of `length` items.
|
|
40
|
+
function sampleIndices(length: number, count: number): number[] {
|
|
41
|
+
if (count >= length) return Array.from({ length }, (_, index) => index);
|
|
42
|
+
const indices: number[] = [];
|
|
43
|
+
for (let i = 0; i < count; i++) {
|
|
44
|
+
indices.push(Math.min(length - 1, Math.round((i * (length - 1)) / (count - 1 || 1))));
|
|
45
|
+
}
|
|
46
|
+
return indices;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function fitFrame(frame: string, terminalColumns: number, terminalRows: number): string {
|
|
50
|
+
const box = boundingBox(frame);
|
|
51
|
+
if (!box) return "";
|
|
52
|
+
|
|
53
|
+
const { lines, left, width } = box;
|
|
54
|
+
const height = lines.length;
|
|
55
|
+
|
|
56
|
+
const availableColumns = Math.max(20, terminalColumns - 2);
|
|
57
|
+
const availableRows = Math.max(6, terminalRows - VERTICAL_CHROME_ROWS);
|
|
58
|
+
|
|
59
|
+
// Single uniform scale so the whole logo fits without distorting its aspect.
|
|
60
|
+
const scale = Math.min(1, availableColumns / Math.max(1, width), availableRows / height);
|
|
61
|
+
const targetHeight = Math.max(1, Math.round(height * scale));
|
|
62
|
+
const targetWidth = Math.max(1, Math.round(width * scale));
|
|
63
|
+
|
|
64
|
+
const rowIndices = sampleIndices(height, targetHeight);
|
|
65
|
+
const columnIndices = sampleIndices(width, targetWidth);
|
|
66
|
+
|
|
67
|
+
return rowIndices
|
|
68
|
+
.map(rowIndex => {
|
|
69
|
+
const line = lines[rowIndex];
|
|
70
|
+
return columnIndices
|
|
71
|
+
.map(columnIndex => line[left + columnIndex] ?? " ")
|
|
72
|
+
.join("")
|
|
73
|
+
.replace(/\s+$/, "");
|
|
74
|
+
})
|
|
75
|
+
.join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function AnimatedLogo(props: { renderer?: any }) {
|
|
79
|
+
const [frameIndex, setFrameIndex] = createSignal(0);
|
|
80
|
+
|
|
81
|
+
// Track the real terminal size from the TUI renderer (process.stdout.rows is
|
|
82
|
+
// unreliable inside the plugin runtime). Re-fit whenever the terminal resizes.
|
|
83
|
+
const renderer = props.renderer;
|
|
84
|
+
const readSize = () => ({
|
|
85
|
+
columns: Math.max(40, renderer?.width ?? process.stdout.columns ?? FALLBACK_COLUMNS),
|
|
86
|
+
rows: Math.max(10, renderer?.height ?? process.stdout.rows ?? FALLBACK_ROWS),
|
|
87
|
+
});
|
|
88
|
+
const [size, setSize] = createSignal(readSize());
|
|
89
|
+
|
|
90
|
+
if (typeof renderer?.on === "function") {
|
|
91
|
+
const onResize = () => setSize(readSize());
|
|
92
|
+
renderer.on("resize", onResize);
|
|
93
|
+
onCleanup(() => {
|
|
94
|
+
if (typeof renderer.off === "function") renderer.off("resize", onResize);
|
|
95
|
+
else if (typeof renderer.removeListener === "function") renderer.removeListener("resize", onResize);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const frameDelayMs = Math.max(16, Math.round(1000 / ASCII_VIDEO_FPS));
|
|
100
|
+
const timer = setInterval(() => {
|
|
101
|
+
setFrameIndex(index => (index + 1) % ASCII_VIDEO_FRAMES.length);
|
|
102
|
+
}, frameDelayMs);
|
|
103
|
+
onCleanup(() => clearInterval(timer));
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<text fg="#fbbf24">
|
|
107
|
+
{fitFrame(ASCII_VIDEO_FRAMES[frameIndex()] ?? "", size().columns, size().rows)}
|
|
108
|
+
</text>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const tui = async (api: any) => {
|
|
113
|
+
api.slots.register({
|
|
114
|
+
slots: {
|
|
115
|
+
home_logo() {
|
|
116
|
+
return <AnimatedLogo renderer={api.renderer} />;
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// The TUI plugin loader reads `mod.default` (strict mode) and requires an object
|
|
123
|
+
// with a tui() function — a bare `export const tui` is silently skipped.
|
|
124
|
+
export default { id: "infinicode-home-logo", tui };
|
|
@@ -1,140 +1,140 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
/**
|
|
3
|
-
* infinicode — mesh command palette
|
|
4
|
-
*
|
|
5
|
-
* Adds the OpenKernel mesh/kernel slash-commands natively to the TUI's `/`
|
|
6
|
-
* command palette. Each command is dispatched to a LOCAL mesh node over HTTP
|
|
7
|
-
* (POST /fed/command → kernel.command()), so the same command surface exposed by
|
|
8
|
-
* `infinicode serve`'s console and the CLI is available right inside the TUI.
|
|
9
|
-
*
|
|
10
|
-
* The local node is started (or reused) by `infinicode run`, which injects:
|
|
11
|
-
* INFINICODE_MESH_URL e.g. http://127.0.0.1:47913
|
|
12
|
-
* INFINICODE_MESH_TOKEN optional bearer token
|
|
13
|
-
*
|
|
14
|
-
* Argless commands (/nodes, /running, /activity, /whoami, /mesh-help) run
|
|
15
|
-
* immediately and show output in an alert. Arg-taking commands (/node, /build,
|
|
16
|
-
* /follow, /dispatch, /goal, /mission, /role) open a prompt first.
|
|
17
|
-
*/
|
|
18
|
-
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
19
|
-
|
|
20
|
-
type CommandResult = { ok?: boolean; text?: string; data?: unknown }
|
|
21
|
-
|
|
22
|
-
const meshUrl = () => (process.env.INFINICODE_MESH_URL ?? "http://127.0.0.1:47913").replace(/\/+$/, "")
|
|
23
|
-
const meshToken = () => process.env.INFINICODE_MESH_TOKEN
|
|
24
|
-
|
|
25
|
-
/** POST a slash-command to the local mesh node and return its CommandResult. */
|
|
26
|
-
async function send(input: string): Promise<CommandResult> {
|
|
27
|
-
const token = meshToken()
|
|
28
|
-
try {
|
|
29
|
-
const res = await fetch(`${meshUrl()}/fed/command`, {
|
|
30
|
-
method: "POST",
|
|
31
|
-
headers: {
|
|
32
|
-
"content-type": "application/json",
|
|
33
|
-
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
34
|
-
},
|
|
35
|
-
body: JSON.stringify({ input }),
|
|
36
|
-
signal: AbortSignal.timeout(30_000),
|
|
37
|
-
})
|
|
38
|
-
if (!res.ok) return { ok: false, text: `mesh error ${res.status} — is a node running? (infinicode serve)` }
|
|
39
|
-
return (await res.json()) as CommandResult
|
|
40
|
-
} catch (e) {
|
|
41
|
-
const msg = e instanceof Error ? e.message : String(e)
|
|
42
|
-
return { ok: false, text: `no local mesh node reachable at ${meshUrl()} (${msg})` }
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Render a CommandResult in a scrollable alert dialog. */
|
|
47
|
-
function show(api: TuiPluginApi, title: string, res: CommandResult): void {
|
|
48
|
-
const DialogAlert = api.ui.DialogAlert
|
|
49
|
-
const message = (res.text && res.text.trim()) || (res.ok ? "(no output)" : "command failed")
|
|
50
|
-
api.ui.dialog.setSize("large")
|
|
51
|
-
api.ui.dialog.replace(() => (
|
|
52
|
-
<DialogAlert title={title} message={message} onConfirm={() => api.ui.dialog.clear()} />
|
|
53
|
-
))
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Run a fixed slash-command now and show the result. */
|
|
57
|
-
function runNow(api: TuiPluginApi, title: string, input: string): void {
|
|
58
|
-
api.ui.toast({ variant: "info", title: "mesh", message: `${input} …`, duration: 1200 })
|
|
59
|
-
void send(input).then((res) => show(api, title, res))
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Open a prompt, then dispatch `<slash> <entered args>`. */
|
|
63
|
-
function runWithArgs(
|
|
64
|
-
api: TuiPluginApi,
|
|
65
|
-
title: string,
|
|
66
|
-
slash: string,
|
|
67
|
-
placeholder: string,
|
|
68
|
-
): void {
|
|
69
|
-
const DialogPrompt = api.ui.DialogPrompt
|
|
70
|
-
api.ui.dialog.setSize("medium")
|
|
71
|
-
api.ui.dialog.replace(() => (
|
|
72
|
-
<DialogPrompt
|
|
73
|
-
title={title}
|
|
74
|
-
value=""
|
|
75
|
-
placeholder={placeholder}
|
|
76
|
-
onConfirm={(args: string) => {
|
|
77
|
-
api.ui.dialog.clear()
|
|
78
|
-
const input = args && args.trim() ? `${slash} ${args.trim()}` : slash
|
|
79
|
-
api.ui.toast({ variant: "info", title: "mesh", message: `${input} …`, duration: 1200 })
|
|
80
|
-
void send(input).then((res) => show(api, title, res))
|
|
81
|
-
}}
|
|
82
|
-
onCancel={() => api.ui.dialog.clear()}
|
|
83
|
-
/>
|
|
84
|
-
))
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
interface Spec {
|
|
88
|
-
name: string
|
|
89
|
-
slashName: string
|
|
90
|
-
title: string
|
|
91
|
-
/** if set, prompt for args first with this placeholder; else run immediately */
|
|
92
|
-
arg?: string
|
|
93
|
-
/** the underlying kernel slash-command (defaults to `/${slashName}`) */
|
|
94
|
-
cmd?: string
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const SPECS: Spec[] = [
|
|
98
|
-
{ name: "mesh_nodes", slashName: "nodes", title: "Mesh nodes" },
|
|
99
|
-
{ name: "mesh_running", slashName: "running", title: "Running tasks" },
|
|
100
|
-
{ name: "mesh_activity", slashName: "activity", title: "Mesh activity" },
|
|
101
|
-
{ name: "mesh_whoami", slashName: "whoami", title: "This node" },
|
|
102
|
-
{ name: "mesh_help", slashName: "mesh-help", title: "Mesh commands", cmd: "/help" },
|
|
103
|
-
{ name: "mesh_node", slashName: "node", title: "Node control", arg: "<node> <message>", cmd: "/node" },
|
|
104
|
-
{ name: "mesh_build", slashName: "build", title: "Phased build", arg: "<node> <goal>", cmd: "/build" },
|
|
105
|
-
{ name: "mesh_dispatch", slashName: "dispatch", title: "Dispatch task", arg: "<node> <task>", cmd: "/dispatch" },
|
|
106
|
-
{ name: "mesh_follow", slashName: "follow", title: "Follow run", arg: "<run-id>", cmd: "/follow" },
|
|
107
|
-
{ name: "mesh_goal", slashName: "goal", title: "New goal", arg: "<goal description>", cmd: "/goal" },
|
|
108
|
-
{ name: "mesh_mission", slashName: "mission", title: "Mission", arg: "<mission description>", cmd: "/mission" },
|
|
109
|
-
{ name: "mesh_role", slashName: "role", title: "Set role", arg: "<role>", cmd: "/role" },
|
|
110
|
-
]
|
|
111
|
-
|
|
112
|
-
const tui: TuiPlugin = async (api, options) => {
|
|
113
|
-
if (options?.enabled === false) return
|
|
114
|
-
|
|
115
|
-
// No keybindings — these are palette-only slash commands. Empty bindings avoids
|
|
116
|
-
// any dependency on the keymap-extras runtime module (which may not resolve for
|
|
117
|
-
// an externally-loaded plugin) so registration can never fail to load.
|
|
118
|
-
api.keymap.registerLayer({
|
|
119
|
-
commands: SPECS.map((s) => ({
|
|
120
|
-
name: s.name,
|
|
121
|
-
title: s.title,
|
|
122
|
-
category: "Mesh",
|
|
123
|
-
namespace: "palette" as const,
|
|
124
|
-
slashName: s.slashName,
|
|
125
|
-
run() {
|
|
126
|
-
const cmd = s.cmd ?? `/${s.slashName}`
|
|
127
|
-
if (s.arg) runWithArgs(api, s.title, cmd, s.arg)
|
|
128
|
-
else runNow(api, s.title, cmd)
|
|
129
|
-
},
|
|
130
|
-
})),
|
|
131
|
-
bindings: [],
|
|
132
|
-
})
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const plugin: TuiPluginModule & { id: string } = {
|
|
136
|
-
id: "infinicode-mesh-commands",
|
|
137
|
-
tui,
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export default plugin
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
/**
|
|
3
|
+
* infinicode — mesh command palette
|
|
4
|
+
*
|
|
5
|
+
* Adds the OpenKernel mesh/kernel slash-commands natively to the TUI's `/`
|
|
6
|
+
* command palette. Each command is dispatched to a LOCAL mesh node over HTTP
|
|
7
|
+
* (POST /fed/command → kernel.command()), so the same command surface exposed by
|
|
8
|
+
* `infinicode serve`'s console and the CLI is available right inside the TUI.
|
|
9
|
+
*
|
|
10
|
+
* The local node is started (or reused) by `infinicode run`, which injects:
|
|
11
|
+
* INFINICODE_MESH_URL e.g. http://127.0.0.1:47913
|
|
12
|
+
* INFINICODE_MESH_TOKEN optional bearer token
|
|
13
|
+
*
|
|
14
|
+
* Argless commands (/nodes, /running, /activity, /whoami, /mesh-help) run
|
|
15
|
+
* immediately and show output in an alert. Arg-taking commands (/node, /build,
|
|
16
|
+
* /follow, /dispatch, /goal, /mission, /role) open a prompt first.
|
|
17
|
+
*/
|
|
18
|
+
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
19
|
+
|
|
20
|
+
type CommandResult = { ok?: boolean; text?: string; data?: unknown }
|
|
21
|
+
|
|
22
|
+
const meshUrl = () => (process.env.INFINICODE_MESH_URL ?? "http://127.0.0.1:47913").replace(/\/+$/, "")
|
|
23
|
+
const meshToken = () => process.env.INFINICODE_MESH_TOKEN
|
|
24
|
+
|
|
25
|
+
/** POST a slash-command to the local mesh node and return its CommandResult. */
|
|
26
|
+
async function send(input: string): Promise<CommandResult> {
|
|
27
|
+
const token = meshToken()
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(`${meshUrl()}/fed/command`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
"content-type": "application/json",
|
|
33
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify({ input }),
|
|
36
|
+
signal: AbortSignal.timeout(30_000),
|
|
37
|
+
})
|
|
38
|
+
if (!res.ok) return { ok: false, text: `mesh error ${res.status} — is a node running? (infinicode serve)` }
|
|
39
|
+
return (await res.json()) as CommandResult
|
|
40
|
+
} catch (e) {
|
|
41
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
42
|
+
return { ok: false, text: `no local mesh node reachable at ${meshUrl()} (${msg})` }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Render a CommandResult in a scrollable alert dialog. */
|
|
47
|
+
function show(api: TuiPluginApi, title: string, res: CommandResult): void {
|
|
48
|
+
const DialogAlert = api.ui.DialogAlert
|
|
49
|
+
const message = (res.text && res.text.trim()) || (res.ok ? "(no output)" : "command failed")
|
|
50
|
+
api.ui.dialog.setSize("large")
|
|
51
|
+
api.ui.dialog.replace(() => (
|
|
52
|
+
<DialogAlert title={title} message={message} onConfirm={() => api.ui.dialog.clear()} />
|
|
53
|
+
))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Run a fixed slash-command now and show the result. */
|
|
57
|
+
function runNow(api: TuiPluginApi, title: string, input: string): void {
|
|
58
|
+
api.ui.toast({ variant: "info", title: "mesh", message: `${input} …`, duration: 1200 })
|
|
59
|
+
void send(input).then((res) => show(api, title, res))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Open a prompt, then dispatch `<slash> <entered args>`. */
|
|
63
|
+
function runWithArgs(
|
|
64
|
+
api: TuiPluginApi,
|
|
65
|
+
title: string,
|
|
66
|
+
slash: string,
|
|
67
|
+
placeholder: string,
|
|
68
|
+
): void {
|
|
69
|
+
const DialogPrompt = api.ui.DialogPrompt
|
|
70
|
+
api.ui.dialog.setSize("medium")
|
|
71
|
+
api.ui.dialog.replace(() => (
|
|
72
|
+
<DialogPrompt
|
|
73
|
+
title={title}
|
|
74
|
+
value=""
|
|
75
|
+
placeholder={placeholder}
|
|
76
|
+
onConfirm={(args: string) => {
|
|
77
|
+
api.ui.dialog.clear()
|
|
78
|
+
const input = args && args.trim() ? `${slash} ${args.trim()}` : slash
|
|
79
|
+
api.ui.toast({ variant: "info", title: "mesh", message: `${input} …`, duration: 1200 })
|
|
80
|
+
void send(input).then((res) => show(api, title, res))
|
|
81
|
+
}}
|
|
82
|
+
onCancel={() => api.ui.dialog.clear()}
|
|
83
|
+
/>
|
|
84
|
+
))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface Spec {
|
|
88
|
+
name: string
|
|
89
|
+
slashName: string
|
|
90
|
+
title: string
|
|
91
|
+
/** if set, prompt for args first with this placeholder; else run immediately */
|
|
92
|
+
arg?: string
|
|
93
|
+
/** the underlying kernel slash-command (defaults to `/${slashName}`) */
|
|
94
|
+
cmd?: string
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const SPECS: Spec[] = [
|
|
98
|
+
{ name: "mesh_nodes", slashName: "nodes", title: "Mesh nodes" },
|
|
99
|
+
{ name: "mesh_running", slashName: "running", title: "Running tasks" },
|
|
100
|
+
{ name: "mesh_activity", slashName: "activity", title: "Mesh activity" },
|
|
101
|
+
{ name: "mesh_whoami", slashName: "whoami", title: "This node" },
|
|
102
|
+
{ name: "mesh_help", slashName: "mesh-help", title: "Mesh commands", cmd: "/help" },
|
|
103
|
+
{ name: "mesh_node", slashName: "node", title: "Node control", arg: "<node> <message>", cmd: "/node" },
|
|
104
|
+
{ name: "mesh_build", slashName: "build", title: "Phased build", arg: "<node> <goal>", cmd: "/build" },
|
|
105
|
+
{ name: "mesh_dispatch", slashName: "dispatch", title: "Dispatch task", arg: "<node> <task>", cmd: "/dispatch" },
|
|
106
|
+
{ name: "mesh_follow", slashName: "follow", title: "Follow run", arg: "<run-id>", cmd: "/follow" },
|
|
107
|
+
{ name: "mesh_goal", slashName: "goal", title: "New goal", arg: "<goal description>", cmd: "/goal" },
|
|
108
|
+
{ name: "mesh_mission", slashName: "mission", title: "Mission", arg: "<mission description>", cmd: "/mission" },
|
|
109
|
+
{ name: "mesh_role", slashName: "role", title: "Set role", arg: "<role>", cmd: "/role" },
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
const tui: TuiPlugin = async (api, options) => {
|
|
113
|
+
if (options?.enabled === false) return
|
|
114
|
+
|
|
115
|
+
// No keybindings — these are palette-only slash commands. Empty bindings avoids
|
|
116
|
+
// any dependency on the keymap-extras runtime module (which may not resolve for
|
|
117
|
+
// an externally-loaded plugin) so registration can never fail to load.
|
|
118
|
+
api.keymap.registerLayer({
|
|
119
|
+
commands: SPECS.map((s) => ({
|
|
120
|
+
name: s.name,
|
|
121
|
+
title: s.title,
|
|
122
|
+
category: "Mesh",
|
|
123
|
+
namespace: "palette" as const,
|
|
124
|
+
slashName: s.slashName,
|
|
125
|
+
run() {
|
|
126
|
+
const cmd = s.cmd ?? `/${s.slashName}`
|
|
127
|
+
if (s.arg) runWithArgs(api, s.title, cmd, s.arg)
|
|
128
|
+
else runNow(api, s.title, cmd)
|
|
129
|
+
},
|
|
130
|
+
})),
|
|
131
|
+
bindings: [],
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const plugin: TuiPluginModule & { id: string } = {
|
|
136
|
+
id: "infinicode-mesh-commands",
|
|
137
|
+
tui,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export default plugin
|