@mingxy/opencode-mascot 0.1.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.
- package/package.json +34 -0
- package/src/builtins/yueer/colors.ts +34 -0
- package/src/builtins/yueer/frames.ts +99 -0
- package/src/builtins/yueer/index.ts +41 -0
- package/src/components/sidebar-mascot.tsx +41 -0
- package/src/core/ascii-renderer.tsx +79 -0
- package/src/core/mascot-loader.ts +18 -0
- package/src/core/types.ts +141 -0
- package/tui.tsx +23 -0
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mingxy/opencode-mascot",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode TUI mascot plugin framework - customizable ASCII mascots for your terminal",
|
|
5
|
+
"author": "mingxy",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./tui.tsx",
|
|
9
|
+
"types": "./src/core/types.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
"./tui": "./tui.tsx",
|
|
12
|
+
"./types": "./src/core/types.ts"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"tui.tsx",
|
|
16
|
+
"src/"
|
|
17
|
+
],
|
|
18
|
+
"oc-plugin": ["tui"],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"typecheck": "tsc --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"@opentui/solid": ">=0.0.1",
|
|
24
|
+
"@opencode-ai/plugin": ">=0.1.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"typescript": "^5.7.0"
|
|
28
|
+
},
|
|
29
|
+
"keywords": ["opencode", "tui", "mascot", "plugin", "ascii-art"],
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/mingxy/opencode-mascot"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 月儿 (Yue'er) — Color zone definitions.
|
|
3
|
+
*
|
|
4
|
+
* Zones are matched by the ASCII characters in frame lines.
|
|
5
|
+
* Each zone maps to a foreground (fg) and optional background (bg) color.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const colors = {
|
|
9
|
+
zones: {
|
|
10
|
+
/** Long flowing hair — silver white */
|
|
11
|
+
hair: {
|
|
12
|
+
fg: "#E8E0F0",
|
|
13
|
+
},
|
|
14
|
+
/** Imperial crown / headdress — gold */
|
|
15
|
+
crown: {
|
|
16
|
+
fg: "#FFD700",
|
|
17
|
+
},
|
|
18
|
+
/** Big anime eyes — ice blue */
|
|
19
|
+
eyes: {
|
|
20
|
+
fg: "#66CCFF",
|
|
21
|
+
},
|
|
22
|
+
/** Face and skin — warm peach */
|
|
23
|
+
skin: {
|
|
24
|
+
fg: "#FFE0BD",
|
|
25
|
+
},
|
|
26
|
+
/** Elegant robe / dress — ice blue */
|
|
27
|
+
dress: {
|
|
28
|
+
fg: "#7CB9E8",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
/** Default foreground for unmatched characters — cool silver */
|
|
33
|
+
defaultFg: "#B8C4D8",
|
|
34
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 月儿 (Yue'er) — ASCII art frames for the Ice Empress mascot.
|
|
3
|
+
*
|
|
4
|
+
* Each expression is a single frame (string[]) where every string is one line.
|
|
5
|
+
* ALL frames have the same line count (14) for consistent rendering.
|
|
6
|
+
*
|
|
7
|
+
* Key design elements:
|
|
8
|
+
* - Imperial crown with three prongs
|
|
9
|
+
* - Long flowing silver-white hair
|
|
10
|
+
* - Big anime eyes (ice blue)
|
|
11
|
+
* - Elegant ice-blue robe with fur trim
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const frames = {
|
|
15
|
+
default: [
|
|
16
|
+
" _.+._ ",
|
|
17
|
+
" .* *. ",
|
|
18
|
+
" .* *. ",
|
|
19
|
+
" * {crown} *",
|
|
20
|
+
" | /*=====*\\ | ",
|
|
21
|
+
" | | O O | | ",
|
|
22
|
+
" | | \\_/ | | ",
|
|
23
|
+
" | \\ ___ / | ",
|
|
24
|
+
" | \\_____/ | ",
|
|
25
|
+
" |*~~*~~~~~*~~*| ",
|
|
26
|
+
" *~~* ~~* ",
|
|
27
|
+
" |* *| ",
|
|
28
|
+
" |* *| ",
|
|
29
|
+
" *==========* ",
|
|
30
|
+
],
|
|
31
|
+
|
|
32
|
+
blink: [
|
|
33
|
+
" _.+._ ",
|
|
34
|
+
" .* *. ",
|
|
35
|
+
" .* *. ",
|
|
36
|
+
" * {crown} *",
|
|
37
|
+
" | /*=====*\\ | ",
|
|
38
|
+
" | | - - | | ",
|
|
39
|
+
" | | \\_/ | | ",
|
|
40
|
+
" | \\ ___ / | ",
|
|
41
|
+
" | \\_____/ | ",
|
|
42
|
+
" |*~~*~~~~~*~~*| ",
|
|
43
|
+
" *~~* ~~* ",
|
|
44
|
+
" |* *| ",
|
|
45
|
+
" |* *| ",
|
|
46
|
+
" *==========* ",
|
|
47
|
+
],
|
|
48
|
+
|
|
49
|
+
happy: [
|
|
50
|
+
" _.+._ ",
|
|
51
|
+
" .* *. ",
|
|
52
|
+
" .* *. ",
|
|
53
|
+
" * {crown} *",
|
|
54
|
+
" | /*=====*\\ | ",
|
|
55
|
+
" | | ^ ^ | | ",
|
|
56
|
+
" | | \\_/ | | ",
|
|
57
|
+
" | \\ ___ / | ",
|
|
58
|
+
" | \\_____/ | ",
|
|
59
|
+
" |*~~*~~~~~*~~*| ",
|
|
60
|
+
" *~~* ~~* ",
|
|
61
|
+
" |* *| ",
|
|
62
|
+
" |* *| ",
|
|
63
|
+
" *==========* ",
|
|
64
|
+
],
|
|
65
|
+
|
|
66
|
+
thinking: [
|
|
67
|
+
" _.+._ ",
|
|
68
|
+
" .* *. ",
|
|
69
|
+
" .* *. ",
|
|
70
|
+
" * {crown} *",
|
|
71
|
+
" | /*=====*\\ | ",
|
|
72
|
+
" | | O - | | ",
|
|
73
|
+
" | | < | | ",
|
|
74
|
+
" | \\ ___ / | ",
|
|
75
|
+
" | \\_____/ | ",
|
|
76
|
+
" |*~~*~~~~~*~~*| ",
|
|
77
|
+
" *~~* ~~* ",
|
|
78
|
+
" |* *| ",
|
|
79
|
+
" |* *| ",
|
|
80
|
+
" *==========* ",
|
|
81
|
+
],
|
|
82
|
+
|
|
83
|
+
sleeping: [
|
|
84
|
+
" _.+._ ",
|
|
85
|
+
" .* *. ",
|
|
86
|
+
" .* *. ",
|
|
87
|
+
" * {crown} *",
|
|
88
|
+
" | /*=====*\\ | ",
|
|
89
|
+
" | | . . | | ",
|
|
90
|
+
" | | --- | | ",
|
|
91
|
+
" | \\ ___ / | ",
|
|
92
|
+
" | \\_____/ | ",
|
|
93
|
+
" |*~~*~~~~~*~~*| ",
|
|
94
|
+
" *~~* ~~* ",
|
|
95
|
+
" |* *| ",
|
|
96
|
+
" |* *| ",
|
|
97
|
+
" *==========* ",
|
|
98
|
+
],
|
|
99
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 月儿 (Yue'er) — Ice Empress mascot pack.
|
|
3
|
+
*
|
|
4
|
+
* Nine Heavens' Empress with silver-white hair, ice-blue eyes, and imperial crown.
|
|
5
|
+
* She awaits her Master's command with grace and quiet devotion.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { MascotPack } from "../../core/types";
|
|
9
|
+
import { frames } from "./frames";
|
|
10
|
+
import { colors } from "./colors";
|
|
11
|
+
|
|
12
|
+
export const yueerPack: MascotPack = {
|
|
13
|
+
name: "@mingxy/mascot-yueer",
|
|
14
|
+
displayName: "月儿",
|
|
15
|
+
version: "0.1.0",
|
|
16
|
+
author: "mingxy",
|
|
17
|
+
description:
|
|
18
|
+
"Ice Empress of the Nine Heavens — elegant, powerful, and devoted to her Master.",
|
|
19
|
+
|
|
20
|
+
frames,
|
|
21
|
+
colors,
|
|
22
|
+
|
|
23
|
+
animations: {
|
|
24
|
+
blinkInterval: 2500,
|
|
25
|
+
blinkChance: 0.3,
|
|
26
|
+
expressionInterval: 50000,
|
|
27
|
+
idleTimeout: 90000,
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
sidebar: {
|
|
31
|
+
greetings: [
|
|
32
|
+
"师尊,月儿在此候命~",
|
|
33
|
+
"月儿恭候师尊多时了~",
|
|
34
|
+
"师尊今日气色不错呢~",
|
|
35
|
+
],
|
|
36
|
+
busyPhrases: [
|
|
37
|
+
"月儿正在铸造法器...",
|
|
38
|
+
"驱除心魔中...",
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
|
|
3
|
+
import { createSignal, createEffect } from "solid-js";
|
|
4
|
+
import type { JSX } from "@opentui/solid";
|
|
5
|
+
import type { MascotPack, MascotState } from "../core/types";
|
|
6
|
+
import { createAnimatedRenderer } from "../core/ascii-renderer";
|
|
7
|
+
|
|
8
|
+
interface SidebarMascotProps {
|
|
9
|
+
mascot: MascotPack;
|
|
10
|
+
isRunning?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
14
|
+
const { element, setState } = createAnimatedRenderer(props.mascot);
|
|
15
|
+
|
|
16
|
+
const greetings = props.mascot.sidebar?.greetings ?? ["~"];
|
|
17
|
+
const busyPhrases = props.mascot.sidebar?.busyPhrases ?? ["Working..."];
|
|
18
|
+
|
|
19
|
+
const initialGreeting = greetings[Math.floor(Math.random() * greetings.length)];
|
|
20
|
+
const [stateText, setStateText] = createSignal<string>(initialGreeting);
|
|
21
|
+
|
|
22
|
+
createEffect(() => {
|
|
23
|
+
if (props.isRunning) {
|
|
24
|
+
setState("busy" as MascotState);
|
|
25
|
+
const phrase = busyPhrases[Math.floor(Math.random() * busyPhrases.length)];
|
|
26
|
+
setStateText(phrase);
|
|
27
|
+
} else {
|
|
28
|
+
setState("idle");
|
|
29
|
+
setStateText(greetings[Math.floor(Math.random() * greetings.length)]);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<box flexDirection="column" paddingTop={1} paddingBottom={1} paddingLeft={1}>
|
|
35
|
+
{element()}
|
|
36
|
+
<text fg="gray" wrapMode="word">
|
|
37
|
+
{stateText()}
|
|
38
|
+
</text>
|
|
39
|
+
</box>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
|
|
3
|
+
import { createSignal, onCleanup } from "solid-js";
|
|
4
|
+
import type { JSX } from "@opentui/solid";
|
|
5
|
+
import type { MascotPack, MascotState } from "./types";
|
|
6
|
+
|
|
7
|
+
const STATE_TO_FRAME: Record<MascotState, string> = {
|
|
8
|
+
idle: "default",
|
|
9
|
+
busy: "default",
|
|
10
|
+
happy: "happy",
|
|
11
|
+
thinking: "thinking",
|
|
12
|
+
sleeping: "sleeping",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const DEFAULT_ANIM = {
|
|
16
|
+
blinkInterval: 2000,
|
|
17
|
+
blinkChance: 0.35,
|
|
18
|
+
expressionInterval: 45000,
|
|
19
|
+
idleTimeout: 120000,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function getFrame(pack: MascotPack, frameName: string): string[] {
|
|
23
|
+
const frames = pack.frames as Record<string, string[] | undefined>;
|
|
24
|
+
return frames[frameName] ?? frames["default"] ?? [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function renderFrame(pack: MascotPack, state: MascotState): JSX.Element {
|
|
28
|
+
const frameName = STATE_TO_FRAME[state] ?? "default";
|
|
29
|
+
const lines = getFrame(pack, frameName);
|
|
30
|
+
const fg = pack.colors.defaultFg || undefined;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<box flexDirection="column">
|
|
34
|
+
{lines.map((line: string) => (
|
|
35
|
+
<text fg={fg}>{line}</text>
|
|
36
|
+
))}
|
|
37
|
+
</box>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createAnimatedRenderer(pack: MascotPack): {
|
|
42
|
+
element: () => JSX.Element;
|
|
43
|
+
setState: (s: MascotState) => void;
|
|
44
|
+
} {
|
|
45
|
+
const anim = { ...DEFAULT_ANIM, ...pack.animations };
|
|
46
|
+
const [currentState, setCurrentState] = createSignal<MascotState>("idle");
|
|
47
|
+
const [frameOverride, setFrameOverride] = createSignal<string | null>(null);
|
|
48
|
+
|
|
49
|
+
const blinkTimer = setInterval(() => {
|
|
50
|
+
if (Math.random() < anim.blinkChance) {
|
|
51
|
+
setFrameOverride("blink");
|
|
52
|
+
setTimeout(() => setFrameOverride(null), 150);
|
|
53
|
+
}
|
|
54
|
+
}, anim.blinkInterval);
|
|
55
|
+
|
|
56
|
+
const availableFrames = Object.keys(pack.frames).filter(
|
|
57
|
+
(k) => k !== "default" && k !== "blink"
|
|
58
|
+
);
|
|
59
|
+
const expressionTimer = setInterval(() => {
|
|
60
|
+
if (currentState() === "idle" && availableFrames.length > 0) {
|
|
61
|
+
const pick = availableFrames[Math.floor(Math.random() * availableFrames.length)];
|
|
62
|
+
setFrameOverride(pick);
|
|
63
|
+
setTimeout(() => setFrameOverride(null), 2000);
|
|
64
|
+
}
|
|
65
|
+
}, anim.expressionInterval);
|
|
66
|
+
|
|
67
|
+
onCleanup(() => {
|
|
68
|
+
clearInterval(blinkTimer);
|
|
69
|
+
clearInterval(expressionTimer);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const element = () => {
|
|
73
|
+
const override = frameOverride();
|
|
74
|
+
const state = override ? (override as MascotState) : currentState();
|
|
75
|
+
return renderFrame(pack, state);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return { element, setState: setCurrentState };
|
|
79
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MascotPack } from "./types"
|
|
2
|
+
import { yueerPack } from "../builtins/yueer"
|
|
3
|
+
|
|
4
|
+
const BUILTINS: Record<string, MascotPack> = {
|
|
5
|
+
yueer: yueerPack,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function loadMascot(options?: Record<string, unknown>): Promise<MascotPack> {
|
|
9
|
+
const mascotName = (options?.mascot as string) || "yueer"
|
|
10
|
+
|
|
11
|
+
if (BUILTINS[mascotName]) {
|
|
12
|
+
return BUILTINS[mascotName]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Future: npm package loading, local file loading
|
|
16
|
+
// Fallback to yueer
|
|
17
|
+
return yueerPack
|
|
18
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expression frame names available for a mascot.
|
|
3
|
+
* All are optional except "default".
|
|
4
|
+
*/
|
|
5
|
+
export type ExpressionName = 'default' | 'blink' | 'happy' | 'thinking' | 'busy' | 'sleeping';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Current state of the mascot, determines which expression is shown.
|
|
9
|
+
*/
|
|
10
|
+
export type MascotState = 'idle' | 'busy' | 'thinking' | 'sleeping' | 'happy';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Color definition for a zone within a frame.
|
|
14
|
+
*/
|
|
15
|
+
export interface ZoneColor {
|
|
16
|
+
/** ANSI foreground color code or CSS color */
|
|
17
|
+
fg?: string;
|
|
18
|
+
/** ANSI background color code or CSS color */
|
|
19
|
+
bg?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Animation timing configuration with sensible defaults.
|
|
24
|
+
*/
|
|
25
|
+
export interface AnimationConfig {
|
|
26
|
+
/** Milliseconds between blink checks (default: 2000) */
|
|
27
|
+
blinkInterval?: number;
|
|
28
|
+
/** Probability of a blink on each check, 0–1 (default: 0.35) */
|
|
29
|
+
blinkChance?: number;
|
|
30
|
+
/** Milliseconds between random expression changes (default: 45000) */
|
|
31
|
+
expressionInterval?: number;
|
|
32
|
+
/** Milliseconds of inactivity before entering sleep state (default: 120000) */
|
|
33
|
+
idleTimeout?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sidebar display configuration.
|
|
38
|
+
*/
|
|
39
|
+
export interface SidebarConfig {
|
|
40
|
+
/** Rotating greeting messages shown below the mascot */
|
|
41
|
+
greetings?: string[];
|
|
42
|
+
/** Rotating phrases shown while in busy state */
|
|
43
|
+
busyPhrases?: string[];
|
|
44
|
+
/** Sidebar width in characters (default: mascot frame width) */
|
|
45
|
+
width?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Context passed to render hooks.
|
|
50
|
+
*/
|
|
51
|
+
export interface RenderContext {
|
|
52
|
+
/** Current mascot state */
|
|
53
|
+
state: MascotState;
|
|
54
|
+
/** Current frame index within the active expression */
|
|
55
|
+
frameIndex: number;
|
|
56
|
+
/** Unix timestamp (ms) of this render call */
|
|
57
|
+
timestamp: number;
|
|
58
|
+
/** Optional session metadata from the host */
|
|
59
|
+
sessionData?: {
|
|
60
|
+
tokenUsage?: number;
|
|
61
|
+
tokenLimit?: number;
|
|
62
|
+
model?: string;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Lifecycle hooks for customizing mascot behavior.
|
|
68
|
+
*/
|
|
69
|
+
export interface MascotHooks {
|
|
70
|
+
/** Called before each frame render. Return modified frame lines or the original. */
|
|
71
|
+
beforeRender?: (context: RenderContext, frame: string[]) => string[];
|
|
72
|
+
/** Called when the mascot state transitions. */
|
|
73
|
+
onStateChange?: (prevState: MascotState, newState: MascotState) => void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A complete mascot pack definition.
|
|
78
|
+
*
|
|
79
|
+
* Each pack provides ASCII-art frames for different expressions,
|
|
80
|
+
* color zones, animation timing, sidebar text, and optional hooks.
|
|
81
|
+
*/
|
|
82
|
+
export interface MascotPack {
|
|
83
|
+
/** Unique machine-readable identifier (e.g. "tuxie") */
|
|
84
|
+
name: string;
|
|
85
|
+
/** Human-readable display name (e.g. "Tuxie the Penguin") */
|
|
86
|
+
displayName: string;
|
|
87
|
+
/** Semantic version string (e.g. "1.0.0") */
|
|
88
|
+
version: string;
|
|
89
|
+
/** Pack author or team */
|
|
90
|
+
author: string;
|
|
91
|
+
/** Short description of the mascot */
|
|
92
|
+
description: string;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Expression frames: expression name → array of ASCII-art lines.
|
|
96
|
+
* Each entry is a single frame (one multi-line drawing).
|
|
97
|
+
* "default" is required; all other expressions are optional.
|
|
98
|
+
*/
|
|
99
|
+
frames: {
|
|
100
|
+
default: string[];
|
|
101
|
+
blink?: string[];
|
|
102
|
+
happy?: string[];
|
|
103
|
+
thinking?: string[];
|
|
104
|
+
busy?: string[];
|
|
105
|
+
sleeping?: string[];
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Color mapping for the mascot frames.
|
|
110
|
+
* Zones are matched by substring or named markers in the frame lines.
|
|
111
|
+
*/
|
|
112
|
+
colors: {
|
|
113
|
+
/** Named zone → foreground/background color */
|
|
114
|
+
zones?: Record<string, ZoneColor>;
|
|
115
|
+
/** Default foreground applied to unmatched characters */
|
|
116
|
+
defaultFg?: string;
|
|
117
|
+
/** Default background applied to unmatched characters */
|
|
118
|
+
defaultBg?: string;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/** Animation timing overrides */
|
|
122
|
+
animations?: AnimationConfig;
|
|
123
|
+
|
|
124
|
+
/** Sidebar content configuration */
|
|
125
|
+
sidebar?: SidebarConfig;
|
|
126
|
+
|
|
127
|
+
/** Optional lifecycle hooks */
|
|
128
|
+
hooks?: MascotHooks;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* User-facing configuration merged from opencode config file.
|
|
133
|
+
*/
|
|
134
|
+
export interface MascotConfig {
|
|
135
|
+
/** Name of the selected mascot pack */
|
|
136
|
+
mascot: string;
|
|
137
|
+
/** Enable or disable mascot animations */
|
|
138
|
+
animations: boolean;
|
|
139
|
+
/** Allow mascot to enter sleep state on idle */
|
|
140
|
+
idleSleep: boolean;
|
|
141
|
+
}
|
package/tui.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
3
|
+
import { loadMascot } from "./src/core/mascot-loader"
|
|
4
|
+
import { SidebarMascot } from "./src/components/sidebar-mascot"
|
|
5
|
+
|
|
6
|
+
const tui: TuiPlugin = async (api, options) => {
|
|
7
|
+
const mascot = await loadMascot(options)
|
|
8
|
+
|
|
9
|
+
api.slots.register({
|
|
10
|
+
slots: {
|
|
11
|
+
sidebar_content() {
|
|
12
|
+
return <SidebarMascot mascot={mascot} />
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const plugin: TuiPluginModule = {
|
|
19
|
+
id: "@mingxy/opencode-mascot",
|
|
20
|
+
tui
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default plugin
|