ui-chan-mcp 0.5.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/.claude-plugin/marketplace.json +13 -0
- package/.claude-plugin/plugin.json +6 -0
- package/.env.example +13 -0
- package/LICENSE +36 -0
- package/README.md +214 -0
- package/agents/mode.md +47 -0
- package/agents/talk.md +23 -0
- package/bin/ui-chan-mcp.mjs +11 -0
- package/bin/ui-chan-node +35 -0
- package/bin/ui-chan.mjs +350 -0
- package/context/AFFINITY.md +94 -0
- package/context/SOUL.md +79 -0
- package/context/VOCABULARY.md +108 -0
- package/cue.schema.json +109 -0
- package/cues/default.json +21 -0
- package/cues/emo_anger.json +14 -0
- package/cues/emo_anger_hi.json +19 -0
- package/cues/emo_anger_lo.json +16 -0
- package/cues/emo_antic.json +18 -0
- package/cues/emo_antic_hi.json +16 -0
- package/cues/emo_antic_lo.json +6 -0
- package/cues/emo_disgust.json +19 -0
- package/cues/emo_disgust_hi.json +20 -0
- package/cues/emo_disgust_lo.json +6 -0
- package/cues/emo_fear.json +19 -0
- package/cues/emo_fear_hi.json +19 -0
- package/cues/emo_fear_lo.json +19 -0
- package/cues/emo_joy.json +17 -0
- package/cues/emo_joy_hi.json +15 -0
- package/cues/emo_joy_lo.json +13 -0
- package/cues/emo_sad.json +15 -0
- package/cues/emo_sad_hi.json +18 -0
- package/cues/emo_sad_lo.json +14 -0
- package/cues/emo_surprise.json +15 -0
- package/cues/emo_surprise_hi.json +14 -0
- package/cues/emo_surprise_lo.json +11 -0
- package/cues/emo_trust.json +16 -0
- package/cues/emo_trust_hi.json +20 -0
- package/cues/emo_trust_lo.json +6 -0
- package/cues/idling_doze_1.json +9 -0
- package/cues/idling_doze_2.json +9 -0
- package/cues/idling_doze_3.json +9 -0
- package/cues/idling_giggle_1.json +9 -0
- package/cues/idling_giggle_2.json +9 -0
- package/cues/idling_lookaround_1.json +10 -0
- package/cues/idling_lookaround_2.json +10 -0
- package/cues/idling_lookaround_3.json +10 -0
- package/cues/idling_ponder.json +9 -0
- package/cues/idling_sigh_1.json +11 -0
- package/cues/idling_sigh_2.json +11 -0
- package/cues/idling_yawn_1.json +9 -0
- package/cues/idling_yawn_2.json +12 -0
- package/cues/idling_yawn_3.json +12 -0
- package/cues/mix_anger_antic.json +16 -0
- package/cues/mix_antic_joy.json +16 -0
- package/cues/mix_disgust_anger.json +17 -0
- package/cues/mix_fear_surprise.json +18 -0
- package/cues/mix_joy_trust.json +19 -0
- package/cues/mix_joy_trust_hi.json +18 -0
- package/cues/mix_sad_disgust.json +12 -0
- package/cues/mix_surprise_sad.json +21 -0
- package/cues/mix_surprise_sad_hi.json +14 -0
- package/cues/mix_trust_fear.json +18 -0
- package/cues/pose_arms_crossed.json +9 -0
- package/cues/pose_banzai.json +17 -0
- package/cues/pose_smug_arms.json +11 -0
- package/cues/pose_smug_hips.json +11 -0
- package/cues/pose_think.json +8 -0
- package/cues/pose_umbrella.json +9 -0
- package/cues/self_guilt.json +20 -0
- package/cues/self_pride.json +10 -0
- package/cues/self_shame.json +19 -0
- package/cues/self_shy.json +16 -0
- package/cues/self_shy_hi.json +17 -0
- package/cues/sys_address.json +9 -0
- package/cues/sys_awkward.json +18 -0
- package/cues/sys_beam.json +18 -0
- package/cues/sys_blank.json +11 -0
- package/cues/sys_care.json +18 -0
- package/cues/sys_confused.json +12 -0
- package/cues/sys_dazed.json +10 -0
- package/cues/sys_dread.json +21 -0
- package/cues/sys_fluster.json +20 -0
- package/cues/sys_intro.json +15 -0
- package/cues/sys_laugh.json +20 -0
- package/cues/sys_neutral.json +5 -0
- package/cues/sys_present.json +9 -0
- package/cues/sys_rain.json +15 -0
- package/cues/sys_refuse.json +18 -0
- package/cues/sys_relief.json +15 -0
- package/cues/sys_sleepy.json +8 -0
- package/cues/sys_smirk.json +10 -0
- package/cues/sys_stream.json +15 -0
- package/cues/sys_success.json +15 -0
- package/cues/sys_think.json +9 -0
- package/dist/app/assets.js +54 -0
- package/dist/app/cues.js +130 -0
- package/dist/app/editor-main.js +199 -0
- package/dist/app/editor-preload.js +17 -0
- package/dist/app/main.js +527 -0
- package/dist/app/persona.js +136 -0
- package/dist/app/preload.js +14 -0
- package/dist/app/state.js +850 -0
- package/dist/app/tts.js +257 -0
- package/dist/mcp-server.js +408 -0
- package/dist/renderer/bundle.js +17983 -0
- package/dist/renderer/editor.html +150 -0
- package/dist/renderer/editor.js +17917 -0
- package/dist/renderer/index.html +425 -0
- package/dist/shared/paths.js +169 -0
- package/dist/shared/set-cue-schema.js +66 -0
- package/dist/shared/types.js +14 -0
- package/docs/CLIENTS.md +110 -0
- package/docs/CUE_AUTHORING.md +76 -0
- package/docs/DEVELOPMENT.md +129 -0
- package/docs/PERSONA.md +61 -0
- package/docs/PSD_LAYERS.md +123 -0
- package/docs/README.md +43 -0
- package/docs/SETUP.html +684 -0
- package/docs/STYLE.md +107 -0
- package/docs/TOOLS.md +33 -0
- package/docs/TROUBLESHOOTING.md +122 -0
- package/docs/TTS.md +61 -0
- package/docs/design/CUE_CATALOG.md +259 -0
- package/docs/images/faces.png +0 -0
- package/docs/images/panel.png +0 -0
- package/hooks/fire-event.js +14 -0
- package/hooks/hooks.json +77 -0
- package/hooks/lib/mascot.js +76 -0
- package/hooks/notify.js +15 -0
- package/hooks/reaction.js +46 -0
- package/hooks/session-start.js +80 -0
- package/package.json +96 -0
- package/persona/ui-chan.md +111 -0
- package/plugins/hermes/ui-chan/__init__.py +98 -0
- package/plugins/hermes/ui-chan/plugin.yaml +7 -0
- package/plugins/opencode/ui-chan.mjs +105 -0
- package/skills/beam/SKILL.md +37 -0
- package/skills/eli14/SKILL.md +139 -0
- package/skills/eli14/references/base.html +495 -0
- package/skills/mode/SKILL.md +67 -0
- package/skills/talk/SKILL.md +27 -0
- package/tools/setup/check-package.mjs +47 -0
- package/tools/setup/clients.mjs +399 -0
- package/tools/setup/doctor.mjs +105 -0
- package/tools/setup/home.mjs +126 -0
- package/tools/setup/prompt.mjs +134 -0
- package/tools/setup/update-check.mjs +8 -0
- package/tools/setup/update.mjs +350 -0
- package/tools/stop-app.mjs +22 -0
- package/ui-chan.config.json +973 -0
package/dist/app/main.js
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const node_child_process_1 = require("node:child_process");
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const electron_1 = require("electron");
|
|
40
|
+
const ws_1 = require("ws");
|
|
41
|
+
const paths_1 = require("../shared/paths");
|
|
42
|
+
const set_cue_schema_1 = require("../shared/set-cue-schema");
|
|
43
|
+
const assets_1 = require("./assets");
|
|
44
|
+
const cues_1 = require("./cues");
|
|
45
|
+
const state_1 = require("./state");
|
|
46
|
+
const tts_1 = require("./tts");
|
|
47
|
+
const projectRoot = path.resolve(__dirname, '..', '..');
|
|
48
|
+
// TTS credentials live in .env / env vars (never in the config). The MCP bridge
|
|
49
|
+
// also forwards them on connect; this covers a manually launched `npm run app`.
|
|
50
|
+
(0, paths_1.loadEnvFiles)(projectRoot);
|
|
51
|
+
// Packaged defaults + the user's ~/.ui-chan overrides (see shared/paths.ts).
|
|
52
|
+
const paths = (0, paths_1.resolvePaths)(projectRoot);
|
|
53
|
+
const config = paths.config;
|
|
54
|
+
const port = Number(process.env.UI_CHAN_PORT ?? config.port ?? 8123);
|
|
55
|
+
const cuesDir = paths.cueDirs;
|
|
56
|
+
const cueSchemaPath = paths.cueSchemaFile;
|
|
57
|
+
let cueErrors = [];
|
|
58
|
+
function loadCurrentCues() {
|
|
59
|
+
const set = (0, cues_1.loadCues)(cuesDir, cueSchemaPath);
|
|
60
|
+
cueErrors = set.errors;
|
|
61
|
+
if (config.tts)
|
|
62
|
+
config.tts.cueVoice = (0, cues_1.extractCueVoice)(set);
|
|
63
|
+
for (const err of set.errors)
|
|
64
|
+
console.error(`[ui-chan] cue error: ${err}`);
|
|
65
|
+
return set.cues;
|
|
66
|
+
}
|
|
67
|
+
let cues = loadCurrentCues();
|
|
68
|
+
let win = null;
|
|
69
|
+
let rendererReady = false;
|
|
70
|
+
let rendererWarnings = [];
|
|
71
|
+
const agents = new Map();
|
|
72
|
+
const pendingCommands = [];
|
|
73
|
+
function findPsd() {
|
|
74
|
+
return (0, assets_1.findPsd)(paths.assetsDirs);
|
|
75
|
+
}
|
|
76
|
+
function sendToRenderer(cmd) {
|
|
77
|
+
if (win && rendererReady) {
|
|
78
|
+
win.webContents.send('ui-chan:command', cmd);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
pendingCommands.push(cmd);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** The agent ういちゃん last spoke for — "who is she talking to right now",
|
|
85
|
+
* which is the question the connections panel exists to answer. */
|
|
86
|
+
let activeAgent = null;
|
|
87
|
+
let nextAgentId = 1;
|
|
88
|
+
/** Is there a newer ui-chan upstream? Checked in a child process because it
|
|
89
|
+
* touches the network (git fetch) and must never stall the mascot. Silent on
|
|
90
|
+
* every failure — "can't tell" and "up to date" look the same on screen, and
|
|
91
|
+
* neither is worth a warning. */
|
|
92
|
+
function checkForUpdate() {
|
|
93
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, [path.join(projectRoot, 'tools', 'setup', 'update-check.mjs'), projectRoot], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
94
|
+
let out = '';
|
|
95
|
+
child.stdout?.on('data', (d) => {
|
|
96
|
+
out += d;
|
|
97
|
+
});
|
|
98
|
+
child.on('close', () => {
|
|
99
|
+
try {
|
|
100
|
+
const st = JSON.parse(out);
|
|
101
|
+
sendToRenderer({
|
|
102
|
+
type: 'update',
|
|
103
|
+
available: Boolean(st.available),
|
|
104
|
+
behind: st.behind,
|
|
105
|
+
blocked: st.blocked ?? null,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
/* not a git install, offline, or git missing — say nothing */
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
child.on('error', () => {
|
|
113
|
+
/* no node? impossible here, but never throw from a timer */
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/** Push the current connection list to the renderer. Called on connect,
|
|
117
|
+
* disconnect, and whenever she starts speaking for someone else — the panel
|
|
118
|
+
* never polls, so it cannot show a stale session. */
|
|
119
|
+
function sendConnections() {
|
|
120
|
+
sendToRenderer({ type: 'connections', agents: [...agents.values()], active: activeAgent });
|
|
121
|
+
}
|
|
122
|
+
const tts = config.tts?.enabled ? new tts_1.VoiSonaTalkClient(config.tts) : null;
|
|
123
|
+
/** Voice can be muted from the panel. This silences the *voice* only — the
|
|
124
|
+
* bubble still appears, because a mascot that goes completely blank looks
|
|
125
|
+
* broken rather than quiet. */
|
|
126
|
+
let muted = false;
|
|
127
|
+
const state = new state_1.UiChanState(config, cues, sendToRenderer, tts
|
|
128
|
+
? (text, cue, adlib) => (muted ? Promise.resolve(null) : tts.synthesize(text, cue, adlib))
|
|
129
|
+
: undefined,
|
|
130
|
+
// OS-wide "seconds since the user last touched keyboard or mouse" — what lets
|
|
131
|
+
// Idling read the user's presence instead of only its own timers.
|
|
132
|
+
() => electron_1.powerMonitor.getSystemIdleTime());
|
|
133
|
+
/** Setup-level problems the agent can't see from tool results alone (no PSD in
|
|
134
|
+
* assets/, TTS on but unusable) surface as warnings so they're diagnosable
|
|
135
|
+
* from a single get_state instead of by reading the app's stderr. */
|
|
136
|
+
function setupWarnings(psdFile) {
|
|
137
|
+
const out = [];
|
|
138
|
+
if (!psdFile) {
|
|
139
|
+
out.push(`立ち絵PSDが見つかりません: ${path.join(paths.home, 'assets')} に PSD を置いてください`);
|
|
140
|
+
}
|
|
141
|
+
const t = tts?.status();
|
|
142
|
+
if (t?.enabled && !t.hasCredentials) {
|
|
143
|
+
out.push(`TTSの資格情報がありません: ${path.join(paths.home, '.env')}(または環境変数)に UI_CHAN_TTS_USERNAME / UI_CHAN_TTS_PASSWORD を設定してください`);
|
|
144
|
+
}
|
|
145
|
+
if (t?.enabled && t.hasCredentials && t.engineUnreachable) {
|
|
146
|
+
out.push(`VoiSona Talk に接続できません (${config.tts?.url}): 起動しているか確認してください`);
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
function buildSnapshot() {
|
|
151
|
+
const psdFile = findPsd();
|
|
152
|
+
const { cueWarning, ...rest } = state.snapshot();
|
|
153
|
+
return {
|
|
154
|
+
psdLoaded: psdFile !== null && rendererReady,
|
|
155
|
+
psdFile: psdFile ? path.basename(psdFile) : null,
|
|
156
|
+
...rest,
|
|
157
|
+
connectedAgents: [...agents.values()],
|
|
158
|
+
availableCues: state.listCues(),
|
|
159
|
+
tts: tts ? tts.status() : { enabled: false },
|
|
160
|
+
warnings: [
|
|
161
|
+
...setupWarnings(psdFile),
|
|
162
|
+
...rendererWarnings,
|
|
163
|
+
...cueErrors,
|
|
164
|
+
...(cueWarning ? [cueWarning] : []),
|
|
165
|
+
],
|
|
166
|
+
affinity: state.affinitySnapshot(),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const toolHandlers = {
|
|
170
|
+
set_cue: (args, agent) => state.setCue(set_cue_schema_1.setCueArgsSchema.parse(args), agent),
|
|
171
|
+
get_state: () => buildSnapshot(),
|
|
172
|
+
clear: () => state.clear(),
|
|
173
|
+
adjust_affinity: (args) => state.adjustAffinity(String(args.direction), String(args.magnitude)),
|
|
174
|
+
event_cue: (args) => state.fireEventCue(String(args.event), { force: args.force === true }),
|
|
175
|
+
};
|
|
176
|
+
function handleDebug(_ws, req) {
|
|
177
|
+
const action = req.debug;
|
|
178
|
+
if (!action) {
|
|
179
|
+
return { id: req.id, ok: false, error: 'missing debug action' };
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
switch (action.type) {
|
|
183
|
+
case 'trigger_idle': {
|
|
184
|
+
const result = state.triggerIdleAction(action.name);
|
|
185
|
+
if (!result.ok) {
|
|
186
|
+
return { id: req.id, ok: false, error: result.error };
|
|
187
|
+
}
|
|
188
|
+
return { id: req.id, ok: true, result };
|
|
189
|
+
}
|
|
190
|
+
case 'list_idle': {
|
|
191
|
+
return { id: req.id, ok: true, result: state.listIdle() };
|
|
192
|
+
}
|
|
193
|
+
case 'list_event_cues': {
|
|
194
|
+
return { id: req.id, ok: true, result: state.listEventCues() };
|
|
195
|
+
}
|
|
196
|
+
case 'trigger_event': {
|
|
197
|
+
// force: the dev asked for this one, so skip cooldown/chance — but the
|
|
198
|
+
// affinity and time gates still apply, so what you see could really play.
|
|
199
|
+
const result = state.fireEventCue(action.event, { force: true });
|
|
200
|
+
if (!result.ok)
|
|
201
|
+
return { id: req.id, ok: false, error: result.error };
|
|
202
|
+
return { id: req.id, ok: true, result };
|
|
203
|
+
}
|
|
204
|
+
case 'preview_cue': {
|
|
205
|
+
return { id: req.id, ok: true, result: state.previewCue(action.cue) };
|
|
206
|
+
}
|
|
207
|
+
case 'set_affinity': {
|
|
208
|
+
const result = state.setAffinity(action.value);
|
|
209
|
+
if (!result.ok) {
|
|
210
|
+
return { id: req.id, ok: false, error: result.error };
|
|
211
|
+
}
|
|
212
|
+
return { id: req.id, ok: true, result };
|
|
213
|
+
}
|
|
214
|
+
case 'interact': {
|
|
215
|
+
state.onInteraction(action.kind ?? 'poke');
|
|
216
|
+
return { id: req.id, ok: true, result: { ok: true } };
|
|
217
|
+
}
|
|
218
|
+
default: {
|
|
219
|
+
return { id: req.id, ok: false, error: `unknown debug action` };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
return { id: req.id, ok: false, error: String(e) };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function handleRequest(ws, req) {
|
|
228
|
+
const agent = agents.get(ws)?.name ?? req.agent ?? 'unknown';
|
|
229
|
+
try {
|
|
230
|
+
switch (req.type) {
|
|
231
|
+
case 'hello': {
|
|
232
|
+
agents.set(ws, {
|
|
233
|
+
id: nextAgentId++,
|
|
234
|
+
name: req.agent ?? 'unknown',
|
|
235
|
+
connectedAt: new Date().toISOString(),
|
|
236
|
+
...(req.identity ?? {}),
|
|
237
|
+
});
|
|
238
|
+
sendConnections();
|
|
239
|
+
if (exitTimer) {
|
|
240
|
+
clearTimeout(exitTimer);
|
|
241
|
+
exitTimer = null;
|
|
242
|
+
}
|
|
243
|
+
if (req.tts?.username && tts) {
|
|
244
|
+
tts.setCredentials(req.tts.username, req.tts.password);
|
|
245
|
+
}
|
|
246
|
+
return { id: req.id, ok: true, result: { server: 'ui-chan-mcp' } };
|
|
247
|
+
}
|
|
248
|
+
case 'tool': {
|
|
249
|
+
const handler = req.tool ? toolHandlers[req.tool] : undefined;
|
|
250
|
+
if (!handler)
|
|
251
|
+
return { id: req.id, ok: false, error: `unknown tool: ${req.tool}` };
|
|
252
|
+
const caller = agents.get(ws)?.id ?? null;
|
|
253
|
+
if (caller !== null && caller !== activeAgent) {
|
|
254
|
+
activeAgent = caller;
|
|
255
|
+
sendConnections();
|
|
256
|
+
}
|
|
257
|
+
return { id: req.id, ok: true, result: handler(req.args ?? {}, agent) };
|
|
258
|
+
}
|
|
259
|
+
case 'debug': {
|
|
260
|
+
return handleDebug(ws, req);
|
|
261
|
+
}
|
|
262
|
+
default:
|
|
263
|
+
return { id: req.id, ok: false, error: `unknown request type` };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
return { id: req.id, ok: false, error: String(e) };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/** Ask the renderer to paint a backdrop and wait for it to have painted.
|
|
271
|
+
* Two frames is enough (one to apply, one to render) and is far more reliable
|
|
272
|
+
* than a fixed sleep. */
|
|
273
|
+
async function setBackdrop(style) {
|
|
274
|
+
if (!win || !rendererReady)
|
|
275
|
+
return;
|
|
276
|
+
sendToRenderer({ type: 'backdrop', style });
|
|
277
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
278
|
+
}
|
|
279
|
+
async function handleScreenshot(req) {
|
|
280
|
+
try {
|
|
281
|
+
if (!win)
|
|
282
|
+
return { id: req.id, ok: false, error: 'no window' };
|
|
283
|
+
const requested = req.args?.path;
|
|
284
|
+
// Installed globally the package dir is not a sane place to write, so
|
|
285
|
+
// screenshots land in the user's home dir whenever there is one.
|
|
286
|
+
const shotRoot = paths.homeExists ? paths.home : projectRoot;
|
|
287
|
+
const out = path.resolve(shotRoot, typeof requested === 'string' && requested.length > 0 ? requested : 'ui-chan-shot.png');
|
|
288
|
+
// The WS server only binds 127.0.0.1, but a caller-supplied path could
|
|
289
|
+
// still try to escape via `..` — keep screenshot writes inside the project.
|
|
290
|
+
if (out !== shotRoot && !out.startsWith(shotRoot + path.sep)) {
|
|
291
|
+
return { id: req.id, ok: false, error: `path must stay within ${shotRoot}` };
|
|
292
|
+
}
|
|
293
|
+
// `background` is any CSS background value ('#fff', a gradient, …) or the
|
|
294
|
+
// named presets in the renderer.透過のままだと、白以外の場所に貼った
|
|
295
|
+
// 瞬間に破綻するので、撮る間だけ背景を敷く。
|
|
296
|
+
const background = typeof req.args?.background === 'string' ? req.args.background : null;
|
|
297
|
+
if (background)
|
|
298
|
+
await setBackdrop(background);
|
|
299
|
+
try {
|
|
300
|
+
const image = await win.webContents.capturePage();
|
|
301
|
+
fs.writeFileSync(out, image.toPNG());
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
if (background)
|
|
305
|
+
await setBackdrop(null);
|
|
306
|
+
}
|
|
307
|
+
return { id: req.id, ok: true, result: { path: out } };
|
|
308
|
+
}
|
|
309
|
+
catch (e) {
|
|
310
|
+
return { id: req.id, ok: false, error: String(e) };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function startWsServer() {
|
|
314
|
+
const wss = new ws_1.WebSocketServer({ port, host: '127.0.0.1' });
|
|
315
|
+
wss.on('connection', (ws) => {
|
|
316
|
+
ws.on('message', (data) => {
|
|
317
|
+
let req;
|
|
318
|
+
try {
|
|
319
|
+
req = JSON.parse(data.toString());
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (req.type === 'screenshot') {
|
|
325
|
+
handleScreenshot(req).then((res) => ws.send(JSON.stringify(res)));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
ws.send(JSON.stringify(handleRequest(ws, req)));
|
|
329
|
+
});
|
|
330
|
+
ws.on('close', () => {
|
|
331
|
+
agents.delete(ws);
|
|
332
|
+
sendConnections();
|
|
333
|
+
scheduleExitIfIdle();
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
wss.on('error', (err) => {
|
|
337
|
+
console.error(`[ui-chan] WebSocket server error: ${err.message}`);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Quit once nothing is connected any more.
|
|
342
|
+
*
|
|
343
|
+
* The app is launched detached (by the MCP server or the SessionStart hook), so
|
|
344
|
+
* without this it outlives every client and has to be killed by hand from the
|
|
345
|
+
* repo. Agents are tracked per WebSocket, which makes "is anyone still there?"
|
|
346
|
+
* exact across windows, apps and other MCP clients alike — and it needs no
|
|
347
|
+
* cooperation from the client, so a session that dies without a goodbye still
|
|
348
|
+
* releases her.
|
|
349
|
+
*
|
|
350
|
+
* The delay matters: restarting Claude Code drops the socket and reconnects a
|
|
351
|
+
* few seconds later, and quitting on the gap would make every restart blink the
|
|
352
|
+
* mascot out of existence. Any reconnection inside the window cancels it.
|
|
353
|
+
*/
|
|
354
|
+
let exitTimer = null;
|
|
355
|
+
function scheduleExitIfIdle() {
|
|
356
|
+
if (exitTimer) {
|
|
357
|
+
clearTimeout(exitTimer);
|
|
358
|
+
exitTimer = null;
|
|
359
|
+
}
|
|
360
|
+
const sec = config.exitAfterLastAgentSec ?? 0;
|
|
361
|
+
if (sec <= 0 || agents.size > 0)
|
|
362
|
+
return;
|
|
363
|
+
exitTimer = setTimeout(() => {
|
|
364
|
+
exitTimer = null;
|
|
365
|
+
if (agents.size > 0)
|
|
366
|
+
return; // someone came back while we waited
|
|
367
|
+
console.error(`[ui-chan] no agents connected for ${sec}s — quitting`);
|
|
368
|
+
electron_1.app.quit();
|
|
369
|
+
}, sec * 1000);
|
|
370
|
+
}
|
|
371
|
+
function createWindow() {
|
|
372
|
+
const { width, height, margin } = config.window;
|
|
373
|
+
const wa = electron_1.screen.getPrimaryDisplay().workArea;
|
|
374
|
+
win = new electron_1.BrowserWindow({
|
|
375
|
+
width,
|
|
376
|
+
height,
|
|
377
|
+
x: wa.x + wa.width - width - margin,
|
|
378
|
+
y: wa.y + wa.height - height - margin,
|
|
379
|
+
transparent: true,
|
|
380
|
+
frame: false,
|
|
381
|
+
resizable: false,
|
|
382
|
+
hasShadow: false,
|
|
383
|
+
alwaysOnTop: true,
|
|
384
|
+
skipTaskbar: true,
|
|
385
|
+
webPreferences: {
|
|
386
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
387
|
+
contextIsolation: true,
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
win.setAlwaysOnTop(true, 'floating');
|
|
391
|
+
win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
392
|
+
win.loadFile(path.join(projectRoot, 'dist', 'renderer', 'index.html'));
|
|
393
|
+
win.on('closed', () => {
|
|
394
|
+
win = null;
|
|
395
|
+
rendererReady = false;
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
const gotLock = electron_1.app.requestSingleInstanceLock();
|
|
399
|
+
if (!gotLock) {
|
|
400
|
+
electron_1.app.quit();
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
electron_1.app.whenReady().then(() => {
|
|
404
|
+
startWsServer();
|
|
405
|
+
createWindow();
|
|
406
|
+
// Once at startup, then every 6 hours: rare enough to be invisible, often
|
|
407
|
+
// enough that a long-running mascot notices a release.
|
|
408
|
+
setTimeout(checkForUpdate, 8_000);
|
|
409
|
+
setInterval(checkForUpdate, 6 * 60 * 60 * 1000);
|
|
410
|
+
(0, cues_1.watchCues)(cuesDir, () => {
|
|
411
|
+
cues = loadCurrentCues();
|
|
412
|
+
state.setCues(cues);
|
|
413
|
+
console.error('[ui-chan] cues reloaded');
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
electron_1.app.on('window-all-closed', () => electron_1.app.quit());
|
|
417
|
+
/** The panel's buttons. Deliberately few and all reversible-or-obvious:
|
|
418
|
+
* anything destructive belongs in the CLI, not in a window that pops open
|
|
419
|
+
* on its own. */
|
|
420
|
+
electron_1.ipcMain.handle('ui-chan:panel-action', (_ev, kind, value) => {
|
|
421
|
+
switch (kind) {
|
|
422
|
+
case 'affinity':
|
|
423
|
+
// The panel is the only place a human can move affinity directly; the
|
|
424
|
+
// agent's own adjust_affinity stays direction+magnitude, so this can't
|
|
425
|
+
// be used to sneak past the asymmetric curve on her behalf.
|
|
426
|
+
if (typeof value === 'number')
|
|
427
|
+
state.setAffinity(value);
|
|
428
|
+
return state.affinitySnapshot();
|
|
429
|
+
case 'affinity:get':
|
|
430
|
+
return state.affinitySnapshot();
|
|
431
|
+
case 'mute':
|
|
432
|
+
muted = true;
|
|
433
|
+
return { muted };
|
|
434
|
+
case 'unmute':
|
|
435
|
+
muted = false;
|
|
436
|
+
return { muted };
|
|
437
|
+
case 'clear':
|
|
438
|
+
state.clear();
|
|
439
|
+
return { ok: true };
|
|
440
|
+
case 'restart':
|
|
441
|
+
electron_1.app.relaunch();
|
|
442
|
+
electron_1.app.quit();
|
|
443
|
+
return { ok: true };
|
|
444
|
+
case 'update': {
|
|
445
|
+
// The update rewrites the very files this process is running from, so
|
|
446
|
+
// it happens in a detached child that outlives us: pull, npm install,
|
|
447
|
+
// build, then start the app again on the new build.
|
|
448
|
+
const child = (0, node_child_process_1.spawn)(path.join(projectRoot, 'bin', 'ui-chan-node'), [path.join(projectRoot, 'bin', 'ui-chan.mjs'), 'update'], { detached: true, stdio: 'ignore' });
|
|
449
|
+
child.unref();
|
|
450
|
+
state.setCue({
|
|
451
|
+
cue: 'sys_think',
|
|
452
|
+
text: '着替えてくる。ちょっと待ってて。',
|
|
453
|
+
reading: 'きがえてくる。ちょっとまってて。',
|
|
454
|
+
}, 'panel');
|
|
455
|
+
return { ok: true };
|
|
456
|
+
}
|
|
457
|
+
case 'quit':
|
|
458
|
+
// Nothing to coordinate: a bridge only launches the app at its own
|
|
459
|
+
// startup, so quitting stays quit until a person starts her again.
|
|
460
|
+
electron_1.app.quit();
|
|
461
|
+
return { ok: true };
|
|
462
|
+
default:
|
|
463
|
+
return { ok: false };
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
electron_1.ipcMain.handle('ui-chan:get-init', () => {
|
|
467
|
+
const psdFile = findPsd();
|
|
468
|
+
return { config, psdAvailable: psdFile !== null, psdFile };
|
|
469
|
+
});
|
|
470
|
+
electron_1.ipcMain.handle('ui-chan:read-psd', () => {
|
|
471
|
+
const psdFile = findPsd();
|
|
472
|
+
if (!psdFile)
|
|
473
|
+
return null;
|
|
474
|
+
return fs.readFileSync(psdFile);
|
|
475
|
+
});
|
|
476
|
+
electron_1.ipcMain.on('ui-chan:ready', () => {
|
|
477
|
+
rendererReady = true;
|
|
478
|
+
state.applyVisual();
|
|
479
|
+
for (const cmd of pendingCommands.splice(0)) {
|
|
480
|
+
win?.webContents.send('ui-chan:command', cmd);
|
|
481
|
+
}
|
|
482
|
+
// The panel is otherwise only fed by *changes*, so a renderer that starts
|
|
483
|
+
// (or reloads) while nobody is connected would sit there showing neither
|
|
484
|
+
// rows nor the empty state. Send the current list once it can receive it.
|
|
485
|
+
sendConnections();
|
|
486
|
+
});
|
|
487
|
+
electron_1.ipcMain.on('ui-chan:interaction', (_ev, kind) => {
|
|
488
|
+
state.onInteraction(kind);
|
|
489
|
+
});
|
|
490
|
+
// Manual window drag (we dropped -webkit-app-region:drag so JS can own the
|
|
491
|
+
// fidget input). While the button is held over her body, the window follows
|
|
492
|
+
// the cursor at a fixed grab offset.
|
|
493
|
+
let dragTimer = null;
|
|
494
|
+
electron_1.ipcMain.on('ui-chan:drag-start', () => {
|
|
495
|
+
if (!win)
|
|
496
|
+
return;
|
|
497
|
+
const start = electron_1.screen.getCursorScreenPoint();
|
|
498
|
+
const [wx, wy] = win.getPosition();
|
|
499
|
+
const offX = start.x - wx;
|
|
500
|
+
const offY = start.y - wy;
|
|
501
|
+
if (dragTimer)
|
|
502
|
+
clearInterval(dragTimer);
|
|
503
|
+
dragTimer = setInterval(() => {
|
|
504
|
+
if (!win)
|
|
505
|
+
return;
|
|
506
|
+
const p = electron_1.screen.getCursorScreenPoint();
|
|
507
|
+
win.setPosition(p.x - offX, p.y - offY);
|
|
508
|
+
}, 16);
|
|
509
|
+
});
|
|
510
|
+
electron_1.ipcMain.on('ui-chan:drag-end', () => {
|
|
511
|
+
if (dragTimer) {
|
|
512
|
+
clearInterval(dragTimer);
|
|
513
|
+
dragTimer = null;
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
electron_1.ipcMain.on('ui-chan:warnings', (_ev, warnings) => {
|
|
517
|
+
// The renderer reports its full warning set on every applyDirectives()
|
|
518
|
+
// call (i.e. on every set_cue), which is normally the same set repeated —
|
|
519
|
+
// only log when it actually changed, or a stuck missing-layer-path
|
|
520
|
+
// warning would spam stderr on every single tool call.
|
|
521
|
+
const changed = JSON.stringify(warnings) !== JSON.stringify(rendererWarnings);
|
|
522
|
+
rendererWarnings = warnings;
|
|
523
|
+
if (changed && warnings.length > 0) {
|
|
524
|
+
console.error(`[ui-chan] layer warnings:\n ${warnings.join('\n ')}`);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.buildCueCatalog = buildCueCatalog;
|
|
37
|
+
exports.buildPersonaText = buildPersonaText;
|
|
38
|
+
// The single implementation of "what the character is", as text an AI can read:
|
|
39
|
+
// persona/ui-chan.md + context/*.md + a Cue catalog generated from cues/.
|
|
40
|
+
//
|
|
41
|
+
// It lives here, in compiled code, because three different callers need exactly
|
|
42
|
+
// the same bytes and used to build them separately:
|
|
43
|
+
// - the MCP server, which ships it as the handshake `instructions` (so any
|
|
44
|
+
// client — Claude Desktop included — gets the character with the tools) and
|
|
45
|
+
// as the `persona` prompt (manual reload after editing the files)
|
|
46
|
+
// - the SessionStart hook, for clients that don't use `instructions`
|
|
47
|
+
// - anything added later that has to hand the character to a model
|
|
48
|
+
//
|
|
49
|
+
// Keeping one copy is what makes "edit the Markdown, restart, done" true: a
|
|
50
|
+
// second implementation drifts, and the drift is invisible until the mascot
|
|
51
|
+
// behaves differently depending on which door the persona came through.
|
|
52
|
+
const fs = __importStar(require("node:fs"));
|
|
53
|
+
const path = __importStar(require("node:path"));
|
|
54
|
+
const types_1 = require("../shared/types");
|
|
55
|
+
const cues_1 = require("./cues");
|
|
56
|
+
/** Order the taxonomy sections so the catalog reads as layers, not a flat list. */
|
|
57
|
+
const CUE_GROUP_ORDER = ['emo', 'mix', 'self', 'sys', 'pose'];
|
|
58
|
+
const CUE_GROUP_TITLE = {
|
|
59
|
+
emo: '基本感情 `emo_<系統>_<lo|無印|hi>`(喜/信頼/恐/驚/悲/嫌悪/怒/期待 × 強度)',
|
|
60
|
+
mix: 'ブレンド `mix_<感情A>_<感情B>`(隣接2感情の混合。名前が構成を表す)',
|
|
61
|
+
self: '自己意識 `self_<感情>`(照れ/恥/罪悪感/誇り)',
|
|
62
|
+
sys: 'システム `sys_<機能>`(状態・進行・特殊)',
|
|
63
|
+
pose: 'ポーズ `pose_<型>`(感情に腕ポーズを重ねた変種)',
|
|
64
|
+
};
|
|
65
|
+
/** The AI-facing Cue catalog, generated fresh from `cues/` on every call — so
|
|
66
|
+
* adding a Cue file can never leave a hand-written list out of date. Cues
|
|
67
|
+
* marked `internal: true` are excluded: they're IdlingCue building blocks
|
|
68
|
+
* (state.ts), not expressions to pick with set_cue. */
|
|
69
|
+
function buildCueCatalog(paths) {
|
|
70
|
+
const { cues, errors } = (0, cues_1.loadCues)(paths.cueDirs, paths.cueSchemaFile);
|
|
71
|
+
const groups = new Map();
|
|
72
|
+
for (const [name, cue] of Object.entries(cues)) {
|
|
73
|
+
if (cue.internal || name === types_1.DEFAULT_CUE_NAME)
|
|
74
|
+
continue;
|
|
75
|
+
const g = name.split('_')[0];
|
|
76
|
+
const list = groups.get(g) ?? [];
|
|
77
|
+
list.push([name, cue]);
|
|
78
|
+
groups.set(g, list);
|
|
79
|
+
}
|
|
80
|
+
const keys = [
|
|
81
|
+
...CUE_GROUP_ORDER.filter((k) => groups.has(k)),
|
|
82
|
+
...[...groups.keys()].filter((k) => !CUE_GROUP_ORDER.includes(k)).sort(),
|
|
83
|
+
];
|
|
84
|
+
const sections = keys.map((k) => {
|
|
85
|
+
const lines = (groups.get(k) ?? [])
|
|
86
|
+
.sort(([a], [b]) => a.localeCompare(b)) // by name, not the label-suffixed line
|
|
87
|
+
.map(([name, cue]) => `- \`${name}\`${cue.label ? `(${cue.label})` : ''}${cue.description ? ` — ${cue.description}` : ''}`);
|
|
88
|
+
return `### ${CUE_GROUP_TITLE[k] ?? k}\n${lines.join('\n')}`;
|
|
89
|
+
});
|
|
90
|
+
const warning = errors.length > 0 ? `\n\n(cue読み込みエラー: ${errors.join('; ')})` : '';
|
|
91
|
+
return ('## 利用可能なCue一覧(set_cueのcue引数。cues/から自動生成)\n\n' +
|
|
92
|
+
'Cue名は構造的:接頭辞が層(emo=基本感情 / mix=ブレンド / self=自己意識 / sys=システム / pose=ポーズ)、' +
|
|
93
|
+
'強度は `_lo`(弱)/無印/`_hi`(強)。系統から辿って選ぶ。\n\n' +
|
|
94
|
+
`${sections.join('\n\n')}${warning}`);
|
|
95
|
+
}
|
|
96
|
+
/** persona file + every `context/*.md` in filename order + the Cue catalog.
|
|
97
|
+
* Missing pieces degrade to a note rather than throwing: a half-formed persona
|
|
98
|
+
* is far better than a mascot that refuses to start. */
|
|
99
|
+
function buildPersonaText(paths) {
|
|
100
|
+
const parts = [];
|
|
101
|
+
const personaPath = paths.personaFile;
|
|
102
|
+
try {
|
|
103
|
+
parts.push(fs.readFileSync(personaPath, 'utf-8'));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
parts.push(`ペルソナファイルが見つかりません: ${personaPath}\nこのパスに人格定義のMarkdownを作成してください。`);
|
|
107
|
+
}
|
|
108
|
+
// Filename-keyed across dirs: a `~/.ui-chan/context/SOUL.md` replaces the
|
|
109
|
+
// packaged one, anything else is appended in filename order.
|
|
110
|
+
const contextFiles = new Map();
|
|
111
|
+
for (const dir of paths.contextDirs) {
|
|
112
|
+
try {
|
|
113
|
+
for (const file of fs.readdirSync(dir).filter((f) => f.endsWith('.md'))) {
|
|
114
|
+
contextFiles.set(file, path.join(dir, file));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
/* unreadable context dir — persona file alone still works */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const file of [...contextFiles.keys()].sort()) {
|
|
122
|
+
try {
|
|
123
|
+
parts.push(fs.readFileSync(contextFiles.get(file), 'utf-8'));
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
/* skip a file that vanished mid-build */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
parts.push(buildCueCatalog(paths));
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
parts.push(`Cue一覧の生成に失敗しました: ${e instanceof Error ? e.message : e}`);
|
|
134
|
+
}
|
|
135
|
+
return parts.join('\n\n---\n\n');
|
|
136
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const electron_1 = require("electron");
|
|
4
|
+
electron_1.contextBridge.exposeInMainWorld('uiChan', {
|
|
5
|
+
getInit: () => electron_1.ipcRenderer.invoke('ui-chan:get-init'),
|
|
6
|
+
readPsd: () => electron_1.ipcRenderer.invoke('ui-chan:read-psd'),
|
|
7
|
+
ready: () => electron_1.ipcRenderer.send('ui-chan:ready'),
|
|
8
|
+
reportWarnings: (warnings) => electron_1.ipcRenderer.send('ui-chan:warnings', warnings),
|
|
9
|
+
onCommand: (cb) => electron_1.ipcRenderer.on('ui-chan:command', (_ev, cmd) => cb(cmd)),
|
|
10
|
+
interaction: (kind) => electron_1.ipcRenderer.send('ui-chan:interaction', kind),
|
|
11
|
+
panelAction: (kind, value) => electron_1.ipcRenderer.invoke('ui-chan:panel-action', kind, value),
|
|
12
|
+
dragStart: () => electron_1.ipcRenderer.send('ui-chan:drag-start'),
|
|
13
|
+
dragEnd: () => electron_1.ipcRenderer.send('ui-chan:drag-end'),
|
|
14
|
+
});
|