synabun 2026.4.21 → 2026.4.25-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +161 -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 +44 -14
- package/neural-interface/public/shared/cdx/cdx-render.js +170 -43
- package/neural-interface/public/shared/cdx/cdx-styles.js +23 -10
- package/neural-interface/public/shared/cdx/cdx-tabs.js +384 -70
- 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 +45 -28
- package/neural-interface/public/shared/ocp/ocp-render.js +50 -43
- 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 +216 -21
- 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 +1281 -252
- 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 ──
|
|
@@ -6,7 +6,7 @@ import { state, emit, on } from '../state.js';
|
|
|
6
6
|
import { reserveRightPanelLayout, clearRightPanelLayout } from '../ui-sidepanel-layout.js';
|
|
7
7
|
import { isClaudePanelOpen, toggleClaudePanel } from '../ui-claude-panel.js';
|
|
8
8
|
import { injectStyles } from './cdx-styles.js';
|
|
9
|
-
import { flushCardBody } from './cdx-render.js';
|
|
9
|
+
import { appendAssistantMarkdownMessage, flushCardBody, renderPostPlanActions } from './cdx-render.js';
|
|
10
10
|
import {
|
|
11
11
|
OPENAI_ICON, ICON_SPARK, ICON_PLUS, ICON_MINIMIZE, ICON_X, ICON_STOP,
|
|
12
12
|
ICON_EDIT, ICON_SHIELD, ICON_PLAN, ICON_BRAIN, SYNABUN_LOGO_ICON,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
createTab, switchTab, closeTab, closeActiveTab, saveTabs, restoreTabs,
|
|
18
18
|
initContextBridge,
|
|
19
19
|
renderPills, renderProjects, updateActiveTabView,
|
|
20
|
-
sendPrompt, dispatchPrompt, interruptTurn,
|
|
20
|
+
sendPrompt, dispatchPrompt, interruptTurn, queueCurrentDraft, steerActiveTurn,
|
|
21
21
|
cycleEffort, togglePlanMode, toggleAutoAccept, syncToolbarState,
|
|
22
22
|
requestModelList, populateModelDropdown,
|
|
23
23
|
setSessionLabel, renameActiveSession, promptNameNewSession, openSettingsPanel, renderSessionMenu,
|
|
@@ -27,11 +27,18 @@ import {
|
|
|
27
27
|
syncQueueTray, editQueueItem, toggleQueuePause, clearQueue,
|
|
28
28
|
startCompaction,
|
|
29
29
|
syncInputEnabled, syncReservedWidth, scheduleThreadSnapshotSave,
|
|
30
|
+
flushAllThreadSnapshots,
|
|
30
31
|
resetStallTimer, stopStallTimer,
|
|
31
32
|
withTab,
|
|
32
33
|
ensureProjectsLoaded,
|
|
33
34
|
} from './cdx-tabs.js';
|
|
34
35
|
|
|
36
|
+
window.addEventListener('beforeunload', () => { try { flushAllThreadSnapshots(); } catch {} });
|
|
37
|
+
window.addEventListener('pagehide', () => { try { flushAllThreadSnapshots(); } catch {} });
|
|
38
|
+
document.addEventListener('visibilitychange', () => {
|
|
39
|
+
if (document.hidden) { try { flushAllThreadSnapshots(); } catch {} }
|
|
40
|
+
});
|
|
41
|
+
|
|
35
42
|
// ═══════════════════════════════════════════
|
|
36
43
|
// Module-level State (panel owns)
|
|
37
44
|
// ═══════════════════════════════════════════
|
|
@@ -68,6 +75,9 @@ let _planContent = '';
|
|
|
68
75
|
let _editedPlanContent = '';
|
|
69
76
|
let _showPostPlanActions = false;
|
|
70
77
|
let _postPlanHeader = 'PLAN COMPLETE';
|
|
78
|
+
let _planTurnActive = false;
|
|
79
|
+
let _planApprovalPending = false;
|
|
80
|
+
let _lastPlanTurnId = '';
|
|
71
81
|
let _planFilePath = '';
|
|
72
82
|
|
|
73
83
|
// ═══════════════════════════════════════════
|
|
@@ -85,8 +95,6 @@ let _host = {};
|
|
|
85
95
|
* flushCardBody(itemState)
|
|
86
96
|
* resetThreadState(opts)
|
|
87
97
|
* appendSystem(text, tone)
|
|
88
|
-
* appendAssistantMarkdownMessage(tab, markdown)
|
|
89
|
-
* renderPostPlanActions(tab, headerText)
|
|
90
98
|
* sendServerRequestReply(requestId, opts)
|
|
91
99
|
* getRequestCardEntry(requestId) -> object|null
|
|
92
100
|
* restoreChangelogButtons(buttons)
|
|
@@ -133,6 +141,9 @@ export function syncBoundState(s) {
|
|
|
133
141
|
_editedPlanContent = s.editedPlanContent ?? _editedPlanContent;
|
|
134
142
|
_showPostPlanActions = s.showPostPlanActions ?? _showPostPlanActions;
|
|
135
143
|
_postPlanHeader = s.postPlanHeader ?? _postPlanHeader;
|
|
144
|
+
_planTurnActive = s.planTurnActive ?? _planTurnActive;
|
|
145
|
+
_planApprovalPending = s.planApprovalPending ?? _planApprovalPending;
|
|
146
|
+
_lastPlanTurnId = s.lastPlanTurnId ?? _lastPlanTurnId;
|
|
136
147
|
_planFilePath = s.planFilePath ?? _planFilePath;
|
|
137
148
|
}
|
|
138
149
|
|
|
@@ -319,7 +330,7 @@ function buildPanel() {
|
|
|
319
330
|
</button>
|
|
320
331
|
</div>
|
|
321
332
|
<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>
|
|
333
|
+
<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
334
|
<button class="cxp-toolbar-toggle" id="cxp-plan-toggle" title="Plan mode: off">${ICON_PLAN}<span class="cxp-btn-label">Plan</span></button>
|
|
324
335
|
<button class="cxp-toolbar-toggle" id="cxp-autoaccept-toggle" title="Auto-accept: off">${ICON_SHIELD}<span class="cxp-btn-label">Auto</span></button>
|
|
325
336
|
<div class="cxp-dropdown" id="cxp-model" data-placeholder="model...">
|
|
@@ -386,12 +397,12 @@ function startupEmptyText() {
|
|
|
386
397
|
function _cdxDocEscHandler(event) {
|
|
387
398
|
if (event.key !== 'Escape') return;
|
|
388
399
|
if (!_visible) return;
|
|
389
|
-
if (!
|
|
400
|
+
if (!activeTab()?.running) return;
|
|
390
401
|
const hintsEl = panelEl('#cxp-slash-hints');
|
|
391
402
|
if (hintsEl && !hintsEl.hidden) return; // let input-level handler dismiss hints first
|
|
392
403
|
event.preventDefault();
|
|
393
404
|
event.stopImmediatePropagation();
|
|
394
|
-
|
|
405
|
+
interruptTurn();
|
|
395
406
|
}
|
|
396
407
|
|
|
397
408
|
function setVisible(nextVisible) {
|
|
@@ -629,7 +640,7 @@ function wireEvents() {
|
|
|
629
640
|
if (event.key === 'Escape') {
|
|
630
641
|
const hintsEl = panelEl('#cxp-slash-hints');
|
|
631
642
|
if (hintsEl && !hintsEl.hidden) { hintsEl.hidden = true; _slashActiveIdx = -1; event.preventDefault(); return; }
|
|
632
|
-
if (
|
|
643
|
+
if (activeTab()?.running) { event.preventDefault(); interruptTurn(); return; }
|
|
633
644
|
}
|
|
634
645
|
if (event.key === 'Tab' && activeTab()?.running) {
|
|
635
646
|
const tab = activeTab();
|
|
@@ -673,15 +684,16 @@ function wireEvents() {
|
|
|
673
684
|
fileInput.value = '';
|
|
674
685
|
});
|
|
675
686
|
send?.addEventListener('click', () => {
|
|
676
|
-
if (
|
|
677
|
-
|
|
687
|
+
if (activeTab()?.running) {
|
|
688
|
+
interruptTurn();
|
|
678
689
|
return;
|
|
679
690
|
}
|
|
680
691
|
sendPrompt();
|
|
681
692
|
});
|
|
682
693
|
minimizeBtn?.addEventListener('click', () => setVisible(false));
|
|
683
694
|
newBtn?.addEventListener('click', () => {
|
|
684
|
-
createTab({ project: activeTab()?.project || storage.getItem(STOR.project) || '' });
|
|
695
|
+
const tab = createTab({ project: activeTab()?.project || storage.getItem(STOR.project) || '' });
|
|
696
|
+
if (!tab) return;
|
|
685
697
|
promptNameNewSession();
|
|
686
698
|
});
|
|
687
699
|
closeBtn?.addEventListener('click', () => closeActiveTab());
|
|
@@ -855,16 +867,22 @@ function wireEvents() {
|
|
|
855
867
|
tab.editedPlanContent = content || tab.editedPlanContent || '';
|
|
856
868
|
tab.showPostPlanActions = true;
|
|
857
869
|
tab.postPlanHeader = 'PLAN UPDATED';
|
|
870
|
+
tab.planApprovalPending = true;
|
|
871
|
+
tab.planTurnActive = false;
|
|
872
|
+
tab.lastPlanTurnId = '';
|
|
858
873
|
if (filePath) tab.planFilePath = filePath;
|
|
859
874
|
if (isActiveTab(tab)) {
|
|
860
875
|
_planContent = tab.planContent;
|
|
861
876
|
_editedPlanContent = tab.editedPlanContent;
|
|
862
877
|
_showPostPlanActions = true;
|
|
863
878
|
_postPlanHeader = 'PLAN UPDATED';
|
|
879
|
+
_planApprovalPending = true;
|
|
880
|
+
_planTurnActive = false;
|
|
881
|
+
_lastPlanTurnId = '';
|
|
864
882
|
if (filePath) _planFilePath = filePath;
|
|
865
883
|
}
|
|
866
|
-
|
|
867
|
-
|
|
884
|
+
appendAssistantMarkdownMessage(tab, content || '');
|
|
885
|
+
renderPostPlanActions(tab, 'PLAN UPDATED');
|
|
868
886
|
saveTabs();
|
|
869
887
|
});
|
|
870
888
|
|
|
@@ -896,7 +914,18 @@ function wireEvents() {
|
|
|
896
914
|
if (source && source !== 'codex') return;
|
|
897
915
|
const tab = _tabs.find((entry) => entry.id === tabId) || activeTab();
|
|
898
916
|
if (!tab) return;
|
|
899
|
-
|
|
917
|
+
tab.showPostPlanActions = true;
|
|
918
|
+
tab.planApprovalPending = true;
|
|
919
|
+
tab.planTurnActive = false;
|
|
920
|
+
tab.postPlanHeader = tab.postPlanHeader || 'PLAN COMPLETE';
|
|
921
|
+
if (isActiveTab(tab)) {
|
|
922
|
+
_showPostPlanActions = true;
|
|
923
|
+
_planApprovalPending = true;
|
|
924
|
+
_planTurnActive = false;
|
|
925
|
+
_postPlanHeader = tab.postPlanHeader;
|
|
926
|
+
}
|
|
927
|
+
renderPostPlanActions(tab, tab.postPlanHeader);
|
|
928
|
+
saveTabs();
|
|
900
929
|
});
|
|
901
930
|
|
|
902
931
|
on('changelog-edit-cancelled', () => {
|
|
@@ -942,6 +971,7 @@ function ensurePanel() {
|
|
|
942
971
|
autosizeInput();
|
|
943
972
|
restoreTabs();
|
|
944
973
|
syncInputEnabled();
|
|
974
|
+
window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'codex' } }));
|
|
945
975
|
// Close Codex when a Claude/OpenCode pill is clicked (mutual exclusion)
|
|
946
976
|
const tray = document.getElementById('term-minimized-tray');
|
|
947
977
|
if (tray) {
|