synabun 2026.4.21 → 2026.4.25-beta.2
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/CHANGELOG.md +175 -881
- package/README.md +1 -1
- package/hooks/claude-code/prompt-submit.mjs +39 -18
- package/hooks/claude-code/session-start.mjs +10 -3
- package/hooks/claude-code/shared.mjs +39 -2
- package/hooks/claude-code/stop.mjs +33 -6
- package/mcp-server/src/index.ts +9 -1
- package/mcp-server/src/services/neural-interface.ts +42 -14
- package/mcp-server/src/services/profiles.ts +8 -7
- package/mcp-server/src/tools/browser-advanced.ts +12 -10
- package/mcp-server/src/tools/browser-interact.ts +39 -38
- package/mcp-server/src/tools/browser-navigate.ts +12 -4
- package/mcp-server/src/tools/loop.ts +7 -3
- package/mcp-server/src/tools/profile.ts +19 -1
- package/neural-interface/lib/loop-logger.js +103 -0
- package/neural-interface/lib/mcp-installer.js +109 -0
- package/neural-interface/lib/plugin-installer.js +264 -0
- package/neural-interface/package.json +1 -1
- package/neural-interface/public/shared/api.js +66 -17
- package/neural-interface/public/shared/cdx/cdx-icons.js +5 -4
- package/neural-interface/public/shared/cdx/cdx-panel.js +117 -14
- package/neural-interface/public/shared/cdx/cdx-render.js +170 -43
- package/neural-interface/public/shared/cdx/cdx-styles.js +55 -10
- package/neural-interface/public/shared/cdx/cdx-tabs.js +394 -72
- package/neural-interface/public/shared/cli-status.js +157 -0
- package/neural-interface/public/shared/constants.js +3 -2
- package/neural-interface/public/shared/html-shell.js +32 -11
- package/neural-interface/public/shared/ocp/ocp-panel.js +127 -28
- package/neural-interface/public/shared/ocp/ocp-render.js +52 -44
- package/neural-interface/public/shared/ocp/ocp-styles.js +33 -0
- package/neural-interface/public/shared/ocp/ocp-tabs.js +693 -82
- package/neural-interface/public/shared/styles.css +635 -0
- package/neural-interface/public/shared/ui-automation-studio.js +38 -12
- package/neural-interface/public/shared/ui-claude-panel.js +322 -15
- package/neural-interface/public/shared/ui-file-explorer.js +38 -18
- package/neural-interface/public/shared/ui-menubar.js +34 -10
- package/neural-interface/public/shared/ui-navbar.js +8 -6
- package/neural-interface/public/shared/ui-schedules-studio.js +1464 -0
- package/neural-interface/public/shared/ui-settings.js +274 -50
- package/neural-interface/public/shared/ui-sidepanel-layout.js +18 -7
- package/neural-interface/public/shared/ui-sidepanel-tray.js +287 -0
- package/neural-interface/public/shared/ui-sync.js +9 -5
- package/neural-interface/public/shared/ui-terminal.js +76 -27
- package/neural-interface/public/shared/ui-tool-updates.js +49 -15
- package/neural-interface/public/shared/ui-tutorial-steps.js +4 -2
- package/neural-interface/public/shared/ui-tutorial.js +24 -18
- package/neural-interface/public/shared/ui-whiteboard.js +79 -31
- package/neural-interface/public/shared/vterm-buffer.js +9 -8
- package/neural-interface/public/variant/2d/main.js +9 -7
- package/neural-interface/public/variant/3d/main.js +10 -8
- package/neural-interface/server.js +1292 -255
- package/package.json +1 -1
- package/skills/leonardo/SKILL.md +5 -1
- package/skills/leonardo/modules/image-prompter.md +5 -1
- package/skills/leonardo/modules/video-prompter.md +5 -1
- package/skills/synabun/SKILL.md +44 -26
- package/skills/synabun/modules/audit.md +40 -36
- package/skills/synabun/modules/changelog.md +22 -18
- package/skills/synabun/modules/memorize.md +19 -15
- package/skills/synabun/modules/schedule.md +285 -0
- package/CLAUDE.md +0 -217
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
import {
|
|
3
|
+
existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync,
|
|
4
|
+
lstatSync, renameSync, rmSync,
|
|
5
|
+
} from 'fs';
|
|
6
|
+
import { join, resolve, dirname } from 'path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
|
|
9
|
+
const PLUGINS_ROOT = () => join(os.homedir(), '.claude', 'plugins');
|
|
10
|
+
const INSTALLED_PLUGINS_FILE = () => join(PLUGINS_ROOT(), 'installed_plugins.json');
|
|
11
|
+
const KNOWN_MARKETPLACES_FILE = () => join(PLUGINS_ROOT(), 'known_marketplaces.json');
|
|
12
|
+
|
|
13
|
+
let trackerPath = null;
|
|
14
|
+
export function setTrackerPath(p) { trackerPath = p; }
|
|
15
|
+
const trackerFile = () => trackerPath || join(process.cwd(), 'data', 'synabun-plugins.json');
|
|
16
|
+
|
|
17
|
+
function ensureDir(p) { if (!existsSync(p)) mkdirSync(p, { recursive: true }); }
|
|
18
|
+
|
|
19
|
+
function readJson(p, fallback) {
|
|
20
|
+
try { return existsSync(p) ? JSON.parse(readFileSync(p, 'utf-8')) : fallback; }
|
|
21
|
+
catch { return fallback; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeJsonAtomic(p, obj) {
|
|
25
|
+
ensureDir(dirname(p));
|
|
26
|
+
const tmp = `${p}.tmp.${process.pid}.${Date.now()}`;
|
|
27
|
+
writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf-8');
|
|
28
|
+
try { renameSync(tmp, p); }
|
|
29
|
+
catch { writeFileSync(p, JSON.stringify(obj, null, 2) + '\n', 'utf-8'); try { unlinkSync(tmp); } catch {} }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function runClaude(args, { timeoutMs = 180000 } = {}) {
|
|
33
|
+
const r = spawnSync('claude', args, {
|
|
34
|
+
encoding: 'utf-8',
|
|
35
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
36
|
+
timeout: timeoutMs,
|
|
37
|
+
});
|
|
38
|
+
const stdout = (r.stdout || '').toString();
|
|
39
|
+
const stderr = (r.stderr || '').toString();
|
|
40
|
+
const combined = `${stdout}${stderr ? '\n' + stderr : ''}`.trim();
|
|
41
|
+
if (r.error && r.error.code === 'ENOENT') {
|
|
42
|
+
return { ok: false, code: -1, stdout, stderr, combined, notFound: true };
|
|
43
|
+
}
|
|
44
|
+
return { ok: r.status === 0, code: r.status ?? -1, stdout, stderr, combined };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function claudeNotFoundErr() {
|
|
48
|
+
return { ok: false, error: 'Claude Code CLI not found on PATH. Install `claude` to manage plugins.' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function gitHeadSha(repoPath) {
|
|
52
|
+
if (!repoPath || !existsSync(repoPath)) return 'unknown';
|
|
53
|
+
const r = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: repoPath, encoding: 'utf-8' });
|
|
54
|
+
const out = (r.stdout || '').trim();
|
|
55
|
+
return out || 'unknown';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readTracker() { return readJson(trackerFile(), { version: 1, plugins: {} }); }
|
|
59
|
+
function writeTracker(obj) { writeJsonAtomic(trackerFile(), obj); }
|
|
60
|
+
|
|
61
|
+
function isSynabunSymlinkMarketplace(entry) {
|
|
62
|
+
return entry && entry.source && entry.source.source === 'synabun-symlink';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Purge legacy "synabun-symlink" entries that break CC's marketplace loader,
|
|
66
|
+
// plus orphaned installed_plugins rows whose cache path no longer exists.
|
|
67
|
+
function cleanLegacyState() {
|
|
68
|
+
try {
|
|
69
|
+
const known = readJson(KNOWN_MARKETPLACES_FILE(), null);
|
|
70
|
+
if (known && typeof known === 'object') {
|
|
71
|
+
let changed = false;
|
|
72
|
+
for (const [name, entry] of Object.entries(known)) {
|
|
73
|
+
if (isSynabunSymlinkMarketplace(entry)) {
|
|
74
|
+
delete known[name];
|
|
75
|
+
changed = true;
|
|
76
|
+
const mkLink = join(PLUGINS_ROOT(), 'marketplaces', name);
|
|
77
|
+
try { if (lstatSync(mkLink).isSymbolicLink()) unlinkSync(mkLink); } catch {}
|
|
78
|
+
try { rmSync(join(PLUGINS_ROOT(), 'cache', name), { recursive: true, force: true }); } catch {}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (changed) writeJsonAtomic(KNOWN_MARKETPLACES_FILE(), known);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const installed = readJson(INSTALLED_PLUGINS_FILE(), null);
|
|
85
|
+
if (installed && installed.plugins && typeof installed.plugins === 'object') {
|
|
86
|
+
let changed2 = false;
|
|
87
|
+
for (const key of Object.keys(installed.plugins)) {
|
|
88
|
+
const list = installed.plugins[key] || [];
|
|
89
|
+
const filtered = list.filter(e => e && e.installPath && existsSync(e.installPath));
|
|
90
|
+
if (filtered.length !== list.length) {
|
|
91
|
+
if (filtered.length) installed.plugins[key] = filtered;
|
|
92
|
+
else delete installed.plugins[key];
|
|
93
|
+
changed2 = true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (changed2) writeJsonAtomic(INSTALLED_PLUGINS_FILE(), installed);
|
|
97
|
+
}
|
|
98
|
+
} catch {}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parsePluginManifest(repoPath) {
|
|
102
|
+
const pluginJson = readJson(join(repoPath, '.claude-plugin', 'plugin.json'), null);
|
|
103
|
+
if (!pluginJson || !pluginJson.name) return { ok: false, error: `Missing .claude-plugin/plugin.json at ${repoPath}` };
|
|
104
|
+
const marketplaceJson = readJson(join(repoPath, '.claude-plugin', 'marketplace.json'), null);
|
|
105
|
+
const pluginName = String(pluginJson.name).trim();
|
|
106
|
+
const marketplaceName = (marketplaceJson && marketplaceJson.name) || pluginName;
|
|
107
|
+
const hooks = pluginJson.hooks && typeof pluginJson.hooks === 'object' ? Object.keys(pluginJson.hooks) : [];
|
|
108
|
+
return { ok: true, pluginName, marketplaceName, hooks, manifest: pluginJson };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Install via `claude plugin marketplace add` + `claude plugin install`.
|
|
112
|
+
// Prefers githubUrl (CC manages its own clone); falls back to local repoPath.
|
|
113
|
+
export function installClaudePlugin({ repoPath, githubUrl } = {}) {
|
|
114
|
+
cleanLegacyState();
|
|
115
|
+
|
|
116
|
+
if (!repoPath || !existsSync(repoPath)) {
|
|
117
|
+
return { ok: false, error: `Repo path required to read plugin manifest: ${repoPath}` };
|
|
118
|
+
}
|
|
119
|
+
const meta = parsePluginManifest(repoPath);
|
|
120
|
+
if (!meta.ok) return meta;
|
|
121
|
+
const { pluginName, marketplaceName, hooks } = meta;
|
|
122
|
+
|
|
123
|
+
const source = githubUrl || resolve(repoPath);
|
|
124
|
+
|
|
125
|
+
const addRes = runClaude(['plugin', 'marketplace', 'add', source, '--scope', 'user']);
|
|
126
|
+
if (addRes.notFound) return claudeNotFoundErr();
|
|
127
|
+
const alreadyAdded = /already (added|exists|configured|registered)/i.test(addRes.combined);
|
|
128
|
+
if (!addRes.ok && !alreadyAdded) {
|
|
129
|
+
return { ok: false, error: `claude plugin marketplace add failed: ${addRes.combined || `exit ${addRes.code}`}` };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const installRes = runClaude(['plugin', 'install', `${pluginName}@${marketplaceName}`, '--scope', 'user']);
|
|
133
|
+
if (installRes.notFound) return claudeNotFoundErr();
|
|
134
|
+
const alreadyInstalled = /already installed/i.test(installRes.combined);
|
|
135
|
+
if (!installRes.ok && !alreadyInstalled) {
|
|
136
|
+
return { ok: false, error: `claude plugin install failed: ${installRes.combined || `exit ${installRes.code}`}` };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const installed = readJson(INSTALLED_PLUGINS_FILE(), { plugins: {} });
|
|
140
|
+
const key = `${pluginName}@${marketplaceName}`;
|
|
141
|
+
const ccList = installed.plugins?.[key] || [];
|
|
142
|
+
const ccEntry = ccList.find(e => e && e.scope === 'user') || ccList[0] || null;
|
|
143
|
+
const installPath = ccEntry?.installPath || null;
|
|
144
|
+
const gitCommitSha = ccEntry?.gitCommitSha || gitHeadSha(repoPath);
|
|
145
|
+
|
|
146
|
+
const tracker = readTracker();
|
|
147
|
+
if (!tracker.plugins || typeof tracker.plugins !== 'object') tracker.plugins = {};
|
|
148
|
+
tracker.plugins[key] = {
|
|
149
|
+
pluginName,
|
|
150
|
+
marketplaceName,
|
|
151
|
+
source,
|
|
152
|
+
githubUrl: githubUrl || null,
|
|
153
|
+
repoPath: resolve(repoPath),
|
|
154
|
+
gitCommitSha,
|
|
155
|
+
installedAt: tracker.plugins[key]?.installedAt || new Date().toISOString(),
|
|
156
|
+
lastUpdated: new Date().toISOString(),
|
|
157
|
+
};
|
|
158
|
+
writeTracker(tracker);
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
ok: true,
|
|
162
|
+
pluginName,
|
|
163
|
+
marketplaceName,
|
|
164
|
+
installPath,
|
|
165
|
+
marketplacePath: join(PLUGINS_ROOT(), 'marketplaces', marketplaceName),
|
|
166
|
+
key,
|
|
167
|
+
hooks,
|
|
168
|
+
gitCommitSha,
|
|
169
|
+
alreadyInstalled,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function uninstallClaudePlugin({ marketplaceName, pluginName } = {}) {
|
|
174
|
+
cleanLegacyState();
|
|
175
|
+
if (!marketplaceName) return { ok: false, error: 'marketplaceName required' };
|
|
176
|
+
|
|
177
|
+
const tracker = readTracker();
|
|
178
|
+
const managedKeys = Object.keys(tracker.plugins || {}).filter(k => {
|
|
179
|
+
const [pName, mName] = k.split('@');
|
|
180
|
+
if (mName !== marketplaceName) return false;
|
|
181
|
+
if (pluginName && pName !== pluginName) return false;
|
|
182
|
+
return true;
|
|
183
|
+
});
|
|
184
|
+
if (!managedKeys.length) return { ok: false, error: `No SynaBun-managed plugin found for ${marketplaceName}${pluginName ? '/' + pluginName : ''}` };
|
|
185
|
+
|
|
186
|
+
const removedPlugins = [];
|
|
187
|
+
for (const key of managedKeys) {
|
|
188
|
+
const [pName] = key.split('@');
|
|
189
|
+
const r = runClaude(['plugin', 'uninstall', key]);
|
|
190
|
+
if (r.notFound) return claudeNotFoundErr();
|
|
191
|
+
const notFound = /not (installed|found)/i.test(r.combined);
|
|
192
|
+
if (!r.ok && !notFound) {
|
|
193
|
+
return { ok: false, error: `claude plugin uninstall failed: ${r.combined || `exit ${r.code}`}` };
|
|
194
|
+
}
|
|
195
|
+
delete tracker.plugins[key];
|
|
196
|
+
removedPlugins.push(pName);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const remaining = Object.keys(tracker.plugins || {}).some(k => k.endsWith(`@${marketplaceName}`));
|
|
200
|
+
if (!remaining) {
|
|
201
|
+
runClaude(['plugin', 'marketplace', 'remove', marketplaceName]);
|
|
202
|
+
}
|
|
203
|
+
writeTracker(tracker);
|
|
204
|
+
return { ok: true, removedPlugins };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function listSynabunPlugins() {
|
|
208
|
+
const tracker = readTracker();
|
|
209
|
+
const installed = readJson(INSTALLED_PLUGINS_FILE(), { plugins: {} });
|
|
210
|
+
const out = [];
|
|
211
|
+
for (const [key, meta] of Object.entries(tracker.plugins || {})) {
|
|
212
|
+
const ccList = installed.plugins?.[key] || [];
|
|
213
|
+
const ccEntry = ccList.find(e => e && e.scope === 'user') || ccList[0] || null;
|
|
214
|
+
const installPath = ccEntry?.installPath || null;
|
|
215
|
+
const installPresent = installPath ? existsSync(installPath) : false;
|
|
216
|
+
const repoExists = meta.repoPath ? existsSync(meta.repoPath) : true;
|
|
217
|
+
out.push({
|
|
218
|
+
key,
|
|
219
|
+
pluginName: meta.pluginName,
|
|
220
|
+
marketplaceName: meta.marketplaceName,
|
|
221
|
+
scope: ccEntry?.scope || 'user',
|
|
222
|
+
installPath,
|
|
223
|
+
installedAt: meta.installedAt,
|
|
224
|
+
lastUpdated: meta.lastUpdated,
|
|
225
|
+
gitCommitSha: ccEntry?.gitCommitSha || meta.gitCommitSha,
|
|
226
|
+
repoPath: meta.repoPath || null,
|
|
227
|
+
githubUrl: meta.githubUrl || null,
|
|
228
|
+
broken: !ccEntry || !installPresent || !repoExists,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Migration: purge legacy synabun-symlink state + reinstall via CLI for any
|
|
235
|
+
// entries whose original repo path still exists.
|
|
236
|
+
export function repairSynabunPlugins() {
|
|
237
|
+
const before = readJson(KNOWN_MARKETPLACES_FILE(), {}) || {};
|
|
238
|
+
const legacy = Object.entries(before)
|
|
239
|
+
.filter(([, v]) => isSynabunSymlinkMarketplace(v))
|
|
240
|
+
.map(([name, v]) => ({ name, path: v.source?.path || null }));
|
|
241
|
+
|
|
242
|
+
cleanLegacyState();
|
|
243
|
+
|
|
244
|
+
const repaired = [];
|
|
245
|
+
const failed = [];
|
|
246
|
+
for (const { name, path } of legacy) {
|
|
247
|
+
if (!path || !existsSync(path)) {
|
|
248
|
+
failed.push({ marketplaceName: name, error: 'Original repo path missing — cannot reinstall automatically' });
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const meta = parsePluginManifest(path);
|
|
252
|
+
if (!meta.ok) { failed.push({ marketplaceName: name, error: meta.error }); continue; }
|
|
253
|
+
let githubUrl = null;
|
|
254
|
+
try {
|
|
255
|
+
const r = spawnSync('git', ['remote', 'get-url', 'origin'], { cwd: path, encoding: 'utf-8' });
|
|
256
|
+
const u = (r.stdout || '').trim();
|
|
257
|
+
if (/^https?:\/\//.test(u)) githubUrl = u.replace(/\.git$/, '');
|
|
258
|
+
} catch {}
|
|
259
|
+
const res = installClaudePlugin({ repoPath: path, githubUrl });
|
|
260
|
+
if (res.ok) repaired.push({ marketplaceName: name, pluginName: res.pluginName });
|
|
261
|
+
else failed.push({ marketplaceName: name, error: res.error });
|
|
262
|
+
}
|
|
263
|
+
return { repaired, failed };
|
|
264
|
+
}
|
|
@@ -975,6 +975,24 @@ export async function launchLoop(params) {
|
|
|
975
975
|
});
|
|
976
976
|
}
|
|
977
977
|
|
|
978
|
+
export async function fetchLoopLog(terminalSessionId) {
|
|
979
|
+
const res = await fetch(`/api/loop/log/${encodeURIComponent(terminalSessionId)}`);
|
|
980
|
+
if (!res.ok) {
|
|
981
|
+
const text = await res.text().catch(() => '');
|
|
982
|
+
throw new Error(text || `HTTP ${res.status}`);
|
|
983
|
+
}
|
|
984
|
+
return res.text();
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
export async function sendClientLoopLog(terminalSessionId, tag, msg, meta) {
|
|
988
|
+
try {
|
|
989
|
+
return await jsonFetch('/api/loop/client-log', {
|
|
990
|
+
method: 'POST',
|
|
991
|
+
...jsonBody({ terminalSessionId: terminalSessionId || null, tag, msg, meta }),
|
|
992
|
+
});
|
|
993
|
+
} catch { /* logging best-effort */ }
|
|
994
|
+
}
|
|
995
|
+
|
|
978
996
|
export async function stopLoop(terminalSessionId) {
|
|
979
997
|
return jsonFetch('/api/loop/stop', {
|
|
980
998
|
method: 'POST',
|
|
@@ -1004,11 +1022,42 @@ export async function searchMemoriesByCategory(query, category, limit = 15) {
|
|
|
1004
1022
|
});
|
|
1005
1023
|
}
|
|
1006
1024
|
|
|
1007
|
-
// ── Loop Schedules ──
|
|
1008
|
-
|
|
1009
|
-
export async function
|
|
1010
|
-
return jsonFetch('/api/
|
|
1011
|
-
}
|
|
1025
|
+
// ── Loop Schedules ──
|
|
1026
|
+
|
|
1027
|
+
export async function fetchScheduleGroups() {
|
|
1028
|
+
return jsonFetch('/api/schedule-groups');
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
export async function createScheduleGroup(payload) {
|
|
1032
|
+
return jsonFetch('/api/schedule-groups', {
|
|
1033
|
+
method: 'POST',
|
|
1034
|
+
...jsonBody(payload),
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
export async function updateScheduleGroup(id, payload) {
|
|
1039
|
+
return jsonFetch(`/api/schedule-groups/${encodeURIComponent(id)}`, {
|
|
1040
|
+
method: 'PUT',
|
|
1041
|
+
...jsonBody(payload),
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
export async function deleteScheduleGroup(id) {
|
|
1046
|
+
return jsonFetch(`/api/schedule-groups/${encodeURIComponent(id)}`, {
|
|
1047
|
+
method: 'DELETE',
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
export async function reorderScheduleGroups(orderedIds) {
|
|
1052
|
+
return jsonFetch('/api/schedule-groups/reorder', {
|
|
1053
|
+
method: 'POST',
|
|
1054
|
+
...jsonBody({ orderedIds }),
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
export async function fetchSchedules() {
|
|
1059
|
+
return jsonFetch('/api/schedules');
|
|
1060
|
+
}
|
|
1012
1061
|
|
|
1013
1062
|
export async function createSchedule(params) {
|
|
1014
1063
|
return jsonFetch('/api/schedules', {
|
|
@@ -1059,12 +1108,12 @@ export async function fetchScheduleTimers() {
|
|
|
1059
1108
|
|
|
1060
1109
|
// ── Quick Timers ──
|
|
1061
1110
|
|
|
1062
|
-
export async function createQuickTimer(templateId, minutes, { profile, model, usesBrowser } = {}) {
|
|
1063
|
-
return jsonFetch('/api/quick-timer', {
|
|
1064
|
-
method: 'POST',
|
|
1065
|
-
...jsonBody({ templateId, minutes, profile, model, usesBrowser }),
|
|
1066
|
-
});
|
|
1067
|
-
}
|
|
1111
|
+
export async function createQuickTimer(templateId, minutes, { profile, model, effort, usesBrowser } = {}) {
|
|
1112
|
+
return jsonFetch('/api/quick-timer', {
|
|
1113
|
+
method: 'POST',
|
|
1114
|
+
...jsonBody({ templateId, minutes, profile, model, effort, usesBrowser }),
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1068
1117
|
|
|
1069
1118
|
export async function fetchQuickTimers() {
|
|
1070
1119
|
return jsonFetch('/api/quick-timers');
|
|
@@ -1076,12 +1125,12 @@ export async function cancelQuickTimer(id) {
|
|
|
1076
1125
|
});
|
|
1077
1126
|
}
|
|
1078
1127
|
|
|
1079
|
-
export async function triggerQuickTimerNow(templateId, { profile, model, usesBrowser } = {}) {
|
|
1080
|
-
return jsonFetch('/api/quick-timer/now', {
|
|
1081
|
-
method: 'POST',
|
|
1082
|
-
...jsonBody({ templateId, profile, model, usesBrowser }),
|
|
1083
|
-
});
|
|
1084
|
-
}
|
|
1128
|
+
export async function triggerQuickTimerNow(templateId, { profile, model, effort, usesBrowser } = {}) {
|
|
1129
|
+
return jsonFetch('/api/quick-timer/now', {
|
|
1130
|
+
method: 'POST',
|
|
1131
|
+
...jsonBody({ templateId, profile, model, effort, usesBrowser }),
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1085
1134
|
|
|
1086
1135
|
// ── Isolated Agents ──
|
|
1087
1136
|
|
|
@@ -49,13 +49,12 @@ export const STOR = {
|
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
// ── Effort Levels ──
|
|
52
|
-
export const EFFORT_LEVELS = ['off', 'low', 'medium', 'high'];
|
|
52
|
+
export const EFFORT_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'];
|
|
53
53
|
|
|
54
54
|
// ── Panel Identity ──
|
|
55
55
|
export const PANEL_OWNER = 'codex-sidepanel';
|
|
56
56
|
|
|
57
|
-
// ── Limits ──
|
|
58
|
-
export const MAX_TABS = 10;
|
|
57
|
+
// ── Snapshot Limits ──
|
|
59
58
|
export const MAX_THREAD_SNAPSHOTS = 18;
|
|
60
59
|
export const MAX_THREAD_SNAPSHOT_CHARS = 2_000_000;
|
|
61
60
|
|
|
@@ -69,7 +68,9 @@ CRITICAL — When you need clarification during planning:
|
|
|
69
68
|
1. Use request_user_input to ask concise multiple-choice questions.
|
|
70
69
|
2. After calling request_user_input, stop and wait for the user's reply before continuing.
|
|
71
70
|
3. Do NOT ask clarification questions as plain text.
|
|
72
|
-
4. Do NOT make code changes until the plan is approved
|
|
71
|
+
4. Do NOT make code changes until the plan is approved.
|
|
72
|
+
5. When the plan is ready, stop after the plan. Do NOT continue into implementation.
|
|
73
|
+
6. The sidepanel will ask the user whether to Edit plan, Compact context, or Continue with implementation.`,
|
|
73
74
|
];
|
|
74
75
|
|
|
75
76
|
// ── Stall Detection ──
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
import { storage } from '../storage.js';
|
|
5
5
|
import { state, emit, on } from '../state.js';
|
|
6
6
|
import { reserveRightPanelLayout, clearRightPanelLayout } from '../ui-sidepanel-layout.js';
|
|
7
|
+
import { subscribeCliStatus, recheckCliStatus, getCliDocUrl, getCliInstallCommand, getCliLabel } from '../cli-status.js';
|
|
7
8
|
import { isClaudePanelOpen, toggleClaudePanel } from '../ui-claude-panel.js';
|
|
8
9
|
import { injectStyles } from './cdx-styles.js';
|
|
9
|
-
import { flushCardBody } from './cdx-render.js';
|
|
10
|
+
import { appendAssistantMarkdownMessage, flushCardBody, renderPostPlanActions } from './cdx-render.js';
|
|
10
11
|
import {
|
|
11
12
|
OPENAI_ICON, ICON_SPARK, ICON_PLUS, ICON_MINIMIZE, ICON_X, ICON_STOP,
|
|
12
13
|
ICON_EDIT, ICON_SHIELD, ICON_PLAN, ICON_BRAIN, SYNABUN_LOGO_ICON,
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
createTab, switchTab, closeTab, closeActiveTab, saveTabs, restoreTabs,
|
|
18
19
|
initContextBridge,
|
|
19
20
|
renderPills, renderProjects, updateActiveTabView,
|
|
20
|
-
sendPrompt, dispatchPrompt, interruptTurn,
|
|
21
|
+
sendPrompt, dispatchPrompt, interruptTurn, queueCurrentDraft, steerActiveTurn,
|
|
21
22
|
cycleEffort, togglePlanMode, toggleAutoAccept, syncToolbarState,
|
|
22
23
|
requestModelList, populateModelDropdown,
|
|
23
24
|
setSessionLabel, renameActiveSession, promptNameNewSession, openSettingsPanel, renderSessionMenu,
|
|
@@ -27,11 +28,18 @@ import {
|
|
|
27
28
|
syncQueueTray, editQueueItem, toggleQueuePause, clearQueue,
|
|
28
29
|
startCompaction,
|
|
29
30
|
syncInputEnabled, syncReservedWidth, scheduleThreadSnapshotSave,
|
|
31
|
+
flushAllThreadSnapshots,
|
|
30
32
|
resetStallTimer, stopStallTimer,
|
|
31
33
|
withTab,
|
|
32
34
|
ensureProjectsLoaded,
|
|
33
35
|
} from './cdx-tabs.js';
|
|
34
36
|
|
|
37
|
+
window.addEventListener('beforeunload', () => { try { flushAllThreadSnapshots(); } catch {} });
|
|
38
|
+
window.addEventListener('pagehide', () => { try { flushAllThreadSnapshots(); } catch {} });
|
|
39
|
+
document.addEventListener('visibilitychange', () => {
|
|
40
|
+
if (document.hidden) { try { flushAllThreadSnapshots(); } catch {} }
|
|
41
|
+
});
|
|
42
|
+
|
|
35
43
|
// ═══════════════════════════════════════════
|
|
36
44
|
// Module-level State (panel owns)
|
|
37
45
|
// ═══════════════════════════════════════════
|
|
@@ -39,6 +47,8 @@ import {
|
|
|
39
47
|
let _panel = null;
|
|
40
48
|
let _visible = false;
|
|
41
49
|
let _resizeBound = false;
|
|
50
|
+
let _cliInstalled = null;
|
|
51
|
+
let _cliUnsub = null;
|
|
42
52
|
|
|
43
53
|
// ═══════════════════════════════════════════
|
|
44
54
|
// Bound State (mirrors active tab — managed by cdx-tabs)
|
|
@@ -68,6 +78,9 @@ let _planContent = '';
|
|
|
68
78
|
let _editedPlanContent = '';
|
|
69
79
|
let _showPostPlanActions = false;
|
|
70
80
|
let _postPlanHeader = 'PLAN COMPLETE';
|
|
81
|
+
let _planTurnActive = false;
|
|
82
|
+
let _planApprovalPending = false;
|
|
83
|
+
let _lastPlanTurnId = '';
|
|
71
84
|
let _planFilePath = '';
|
|
72
85
|
|
|
73
86
|
// ═══════════════════════════════════════════
|
|
@@ -85,8 +98,6 @@ let _host = {};
|
|
|
85
98
|
* flushCardBody(itemState)
|
|
86
99
|
* resetThreadState(opts)
|
|
87
100
|
* appendSystem(text, tone)
|
|
88
|
-
* appendAssistantMarkdownMessage(tab, markdown)
|
|
89
|
-
* renderPostPlanActions(tab, headerText)
|
|
90
101
|
* sendServerRequestReply(requestId, opts)
|
|
91
102
|
* getRequestCardEntry(requestId) -> object|null
|
|
92
103
|
* restoreChangelogButtons(buttons)
|
|
@@ -133,6 +144,9 @@ export function syncBoundState(s) {
|
|
|
133
144
|
_editedPlanContent = s.editedPlanContent ?? _editedPlanContent;
|
|
134
145
|
_showPostPlanActions = s.showPostPlanActions ?? _showPostPlanActions;
|
|
135
146
|
_postPlanHeader = s.postPlanHeader ?? _postPlanHeader;
|
|
147
|
+
_planTurnActive = s.planTurnActive ?? _planTurnActive;
|
|
148
|
+
_planApprovalPending = s.planApprovalPending ?? _planApprovalPending;
|
|
149
|
+
_lastPlanTurnId = s.lastPlanTurnId ?? _lastPlanTurnId;
|
|
136
150
|
_planFilePath = s.planFilePath ?? _planFilePath;
|
|
137
151
|
}
|
|
138
152
|
|
|
@@ -319,7 +333,7 @@ function buildPanel() {
|
|
|
319
333
|
</button>
|
|
320
334
|
</div>
|
|
321
335
|
<div class="cxp-footer-right">
|
|
322
|
-
<button class="cxp-toolbar-toggle" id="cxp-effort-toggle" title="Effort level: off" data-effort="off">${ICON_BRAIN}<span class="cxp-btn-label">Effort</span><span class="cxp-effort-dots"><i></i><i></i><i></i></span></button>
|
|
336
|
+
<button class="cxp-toolbar-toggle" id="cxp-effort-toggle" title="Effort level: off" data-effort="off">${ICON_BRAIN}<span class="cxp-btn-label">Effort</span><span class="cxp-effort-dots"><i></i><i></i><i></i><i></i><i></i></span></button>
|
|
323
337
|
<button class="cxp-toolbar-toggle" id="cxp-plan-toggle" title="Plan mode: off">${ICON_PLAN}<span class="cxp-btn-label">Plan</span></button>
|
|
324
338
|
<button class="cxp-toolbar-toggle" id="cxp-autoaccept-toggle" title="Auto-accept: off">${ICON_SHIELD}<span class="cxp-btn-label">Auto</span></button>
|
|
325
339
|
<div class="cxp-dropdown" id="cxp-model" data-placeholder="model...">
|
|
@@ -379,6 +393,74 @@ function startupEmptyText() {
|
|
|
379
393
|
return '';
|
|
380
394
|
}
|
|
381
395
|
|
|
396
|
+
// ── CLI installation status (banner + send-block) ──
|
|
397
|
+
function renderCliMissingBanner(emptyEl) {
|
|
398
|
+
if (!emptyEl) return;
|
|
399
|
+
const label = getCliLabel('codex');
|
|
400
|
+
const cmd = getCliInstallCommand('codex');
|
|
401
|
+
const url = getCliDocUrl('codex');
|
|
402
|
+
const statusEl = emptyEl.querySelector('.cxp-empty-status');
|
|
403
|
+
if (statusEl) statusEl.style.display = 'none';
|
|
404
|
+
let banner = emptyEl.querySelector('.cxp-cli-missing');
|
|
405
|
+
if (!banner) {
|
|
406
|
+
banner = document.createElement('div');
|
|
407
|
+
banner.className = 'cxp-cli-missing';
|
|
408
|
+
banner.innerHTML = `
|
|
409
|
+
<div class="cxp-cli-missing-title">${label} CLI not installed</div>
|
|
410
|
+
<div class="cxp-cli-missing-body">Run: <code>${cmd}</code></div>
|
|
411
|
+
<div class="cxp-cli-missing-actions">
|
|
412
|
+
<a class="cxp-cli-missing-link" href="${url}" target="_blank" rel="noopener noreferrer">Install guide</a>
|
|
413
|
+
<button class="cxp-cli-missing-recheck" type="button">Re-check</button>
|
|
414
|
+
</div>
|
|
415
|
+
`;
|
|
416
|
+
emptyEl.appendChild(banner);
|
|
417
|
+
const btn = banner.querySelector('.cxp-cli-missing-recheck');
|
|
418
|
+
btn?.addEventListener('click', async () => {
|
|
419
|
+
btn.disabled = true;
|
|
420
|
+
btn.textContent = 'Checking…';
|
|
421
|
+
try { await recheckCliStatus('codex'); }
|
|
422
|
+
finally {
|
|
423
|
+
if (btn.isConnected) {
|
|
424
|
+
btn.disabled = false;
|
|
425
|
+
btn.textContent = 'Re-check';
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function refreshCliBanners() {
|
|
433
|
+
if (!_panel) return;
|
|
434
|
+
const empties = _panel.querySelectorAll('.cxp-empty');
|
|
435
|
+
if (_cliInstalled === null) {
|
|
436
|
+
_panel.classList.add('cxp-cli-blocked');
|
|
437
|
+
for (const el of empties) renderCliMissingBanner(el);
|
|
438
|
+
} else {
|
|
439
|
+
_panel.classList.remove('cxp-cli-blocked');
|
|
440
|
+
for (const el of empties) {
|
|
441
|
+
const banner = el.querySelector('.cxp-cli-missing');
|
|
442
|
+
if (banner) banner.remove();
|
|
443
|
+
const statusEl = el.querySelector('.cxp-empty-status');
|
|
444
|
+
if (statusEl) statusEl.style.display = '';
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// Re-evaluate send button state with new CLI flag
|
|
448
|
+
try { syncInputEnabled(); } catch {}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function isCdxCliInstalled() {
|
|
452
|
+
return _cliInstalled !== null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function ensureCliSubscription() {
|
|
456
|
+
if (_cliUnsub) return;
|
|
457
|
+
_cliUnsub = subscribeCliStatus('codex', (info) => {
|
|
458
|
+
_cliInstalled = info?.installed || null;
|
|
459
|
+
refreshCliBanners();
|
|
460
|
+
});
|
|
461
|
+
document.addEventListener('cdx-empty-rebuilt', () => refreshCliBanners());
|
|
462
|
+
}
|
|
463
|
+
|
|
382
464
|
// ═══════════════════════════════════════════
|
|
383
465
|
// setVisible — Show / Hide Panel
|
|
384
466
|
// ═══════════════════════════════════════════
|
|
@@ -386,12 +468,12 @@ function startupEmptyText() {
|
|
|
386
468
|
function _cdxDocEscHandler(event) {
|
|
387
469
|
if (event.key !== 'Escape') return;
|
|
388
470
|
if (!_visible) return;
|
|
389
|
-
if (!
|
|
471
|
+
if (!activeTab()?.running) return;
|
|
390
472
|
const hintsEl = panelEl('#cxp-slash-hints');
|
|
391
473
|
if (hintsEl && !hintsEl.hidden) return; // let input-level handler dismiss hints first
|
|
392
474
|
event.preventDefault();
|
|
393
475
|
event.stopImmediatePropagation();
|
|
394
|
-
|
|
476
|
+
interruptTurn();
|
|
395
477
|
}
|
|
396
478
|
|
|
397
479
|
function setVisible(nextVisible) {
|
|
@@ -415,6 +497,7 @@ function setVisible(nextVisible) {
|
|
|
415
497
|
loadCurrentProfile();
|
|
416
498
|
loadRecallProfile();
|
|
417
499
|
panelEl('#cxp-input')?.focus();
|
|
500
|
+
refreshCliBanners();
|
|
418
501
|
} else {
|
|
419
502
|
document.removeEventListener('keydown', _cdxDocEscHandler, { capture: true });
|
|
420
503
|
}
|
|
@@ -629,7 +712,7 @@ function wireEvents() {
|
|
|
629
712
|
if (event.key === 'Escape') {
|
|
630
713
|
const hintsEl = panelEl('#cxp-slash-hints');
|
|
631
714
|
if (hintsEl && !hintsEl.hidden) { hintsEl.hidden = true; _slashActiveIdx = -1; event.preventDefault(); return; }
|
|
632
|
-
if (
|
|
715
|
+
if (activeTab()?.running) { event.preventDefault(); interruptTurn(); return; }
|
|
633
716
|
}
|
|
634
717
|
if (event.key === 'Tab' && activeTab()?.running) {
|
|
635
718
|
const tab = activeTab();
|
|
@@ -673,15 +756,16 @@ function wireEvents() {
|
|
|
673
756
|
fileInput.value = '';
|
|
674
757
|
});
|
|
675
758
|
send?.addEventListener('click', () => {
|
|
676
|
-
if (
|
|
677
|
-
|
|
759
|
+
if (activeTab()?.running) {
|
|
760
|
+
interruptTurn();
|
|
678
761
|
return;
|
|
679
762
|
}
|
|
680
763
|
sendPrompt();
|
|
681
764
|
});
|
|
682
765
|
minimizeBtn?.addEventListener('click', () => setVisible(false));
|
|
683
766
|
newBtn?.addEventListener('click', () => {
|
|
684
|
-
createTab({ project: activeTab()?.project || storage.getItem(STOR.project) || '' });
|
|
767
|
+
const tab = createTab({ project: activeTab()?.project || storage.getItem(STOR.project) || '' });
|
|
768
|
+
if (!tab) return;
|
|
685
769
|
promptNameNewSession();
|
|
686
770
|
});
|
|
687
771
|
closeBtn?.addEventListener('click', () => closeActiveTab());
|
|
@@ -855,16 +939,22 @@ function wireEvents() {
|
|
|
855
939
|
tab.editedPlanContent = content || tab.editedPlanContent || '';
|
|
856
940
|
tab.showPostPlanActions = true;
|
|
857
941
|
tab.postPlanHeader = 'PLAN UPDATED';
|
|
942
|
+
tab.planApprovalPending = true;
|
|
943
|
+
tab.planTurnActive = false;
|
|
944
|
+
tab.lastPlanTurnId = '';
|
|
858
945
|
if (filePath) tab.planFilePath = filePath;
|
|
859
946
|
if (isActiveTab(tab)) {
|
|
860
947
|
_planContent = tab.planContent;
|
|
861
948
|
_editedPlanContent = tab.editedPlanContent;
|
|
862
949
|
_showPostPlanActions = true;
|
|
863
950
|
_postPlanHeader = 'PLAN UPDATED';
|
|
951
|
+
_planApprovalPending = true;
|
|
952
|
+
_planTurnActive = false;
|
|
953
|
+
_lastPlanTurnId = '';
|
|
864
954
|
if (filePath) _planFilePath = filePath;
|
|
865
955
|
}
|
|
866
|
-
|
|
867
|
-
|
|
956
|
+
appendAssistantMarkdownMessage(tab, content || '');
|
|
957
|
+
renderPostPlanActions(tab, 'PLAN UPDATED');
|
|
868
958
|
saveTabs();
|
|
869
959
|
});
|
|
870
960
|
|
|
@@ -896,7 +986,18 @@ function wireEvents() {
|
|
|
896
986
|
if (source && source !== 'codex') return;
|
|
897
987
|
const tab = _tabs.find((entry) => entry.id === tabId) || activeTab();
|
|
898
988
|
if (!tab) return;
|
|
899
|
-
|
|
989
|
+
tab.showPostPlanActions = true;
|
|
990
|
+
tab.planApprovalPending = true;
|
|
991
|
+
tab.planTurnActive = false;
|
|
992
|
+
tab.postPlanHeader = tab.postPlanHeader || 'PLAN COMPLETE';
|
|
993
|
+
if (isActiveTab(tab)) {
|
|
994
|
+
_showPostPlanActions = true;
|
|
995
|
+
_planApprovalPending = true;
|
|
996
|
+
_planTurnActive = false;
|
|
997
|
+
_postPlanHeader = tab.postPlanHeader;
|
|
998
|
+
}
|
|
999
|
+
renderPostPlanActions(tab, tab.postPlanHeader);
|
|
1000
|
+
saveTabs();
|
|
900
1001
|
});
|
|
901
1002
|
|
|
902
1003
|
on('changelog-edit-cancelled', () => {
|
|
@@ -942,6 +1043,8 @@ function ensurePanel() {
|
|
|
942
1043
|
autosizeInput();
|
|
943
1044
|
restoreTabs();
|
|
944
1045
|
syncInputEnabled();
|
|
1046
|
+
ensureCliSubscription();
|
|
1047
|
+
window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'codex' } }));
|
|
945
1048
|
// Close Codex when a Claude/OpenCode pill is clicked (mutual exclusion)
|
|
946
1049
|
const tray = document.getElementById('term-minimized-tray');
|
|
947
1050
|
if (tray) {
|