claude-code-session-manager 0.15.0 → 0.17.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/dist/assets/{TiptapBody-D0tfDVZb.js → TiptapBody-CQUVYNDX.js} +1 -1
- package/dist/assets/{cssMode-C3tZkaJ9.js → cssMode-8ncTkitH.js} +1 -1
- package/dist/assets/{freemarker2-DEh8tC5X.js → freemarker2-DiWr5tWQ.js} +1 -1
- package/dist/assets/{handlebars-D_6KmsOK.js → handlebars-BdG5bj_X.js} +1 -1
- package/dist/assets/{html-DF1KVjzv.js → html-BjT3cHU8.js} +1 -1
- package/dist/assets/{htmlMode-DEakPokt.js → htmlMode-B6lopeCZ.js} +1 -1
- package/dist/assets/{index-D-kX3T0V.css → index-CSotRGGc.css} +1 -1
- package/dist/assets/{index-Zg61GP50.js → index-DCaakMzv.js} +1388 -1323
- package/dist/assets/{javascript-Du7a359D.js → javascript-BCMuo5_4.js} +1 -1
- package/dist/assets/{jsonMode-W3BJwbUD.js → jsonMode-1KRBrXXK.js} +1 -1
- package/dist/assets/{liquid-DDj1fqca.js → liquid-ls0rkUL4.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-uyEbiR-d.js → lspLanguageFeatures-BA-sGQJB.js} +1 -1
- package/dist/assets/{mdx-DUqSETvC.js → mdx-T7BxS13S.js} +1 -1
- package/dist/assets/{python-D7S2lUAn.js → python-CYW9xWwh.js} +1 -1
- package/dist/assets/{razor-19nfZNaZ.js → razor-D06Ehp7u.js} +1 -1
- package/dist/assets/{tsMode-CQke5zpL.js → tsMode-yKkshrv7.js} +1 -1
- package/dist/assets/{typescript-D4ge0PUF.js → typescript-Dlkgf9PI.js} +1 -1
- package/dist/assets/{xml-D42QDS7q.js → xml-BSp0bNTm.js} +1 -1
- package/dist/assets/{yaml-CEiz9NyU.js → yaml-C8041Bt_.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/main/files.cjs +35 -4
- package/src/main/index.cjs +84 -4
- package/src/preload/api.d.ts +1 -1
package/src/main/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { app, BrowserWindow, ipcMain, dialog, Menu, session, systemPreferences, globalShortcut, shell, clipboard } = require('electron');
|
|
1
|
+
const { app, BrowserWindow, ipcMain, dialog, Menu, session, systemPreferences, globalShortcut, shell, clipboard, powerSaveBlocker, protocol } = require('electron');
|
|
2
2
|
const { spawn, execFile, execFileSync } = require('node:child_process');
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
const fs = require('node:fs');
|
|
@@ -35,11 +35,14 @@ const searchIpc = require('./search.cjs');
|
|
|
35
35
|
const repoAnalyzer = require('./repoAnalyzer.cjs');
|
|
36
36
|
const hivesIpc = require('./hives.cjs');
|
|
37
37
|
const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
|
|
38
|
-
const { checkInsideHome } = require('./lib/insideHome.cjs');
|
|
38
|
+
const { checkInsideHome, assertInsideHome } = require('./lib/insideHome.cjs');
|
|
39
39
|
const { openInEditor, openFileInEditor, openInFinder, openInTerminal } = require('./lib/openExternalApp.cjs');
|
|
40
40
|
|
|
41
41
|
let mainWindow = null;
|
|
42
42
|
let rebooting = false;
|
|
43
|
+
// powerSaveBlocker handle — keeps the system from suspending while the app runs
|
|
44
|
+
// so the scheduler's polling and jobs aren't frozen. -1 = not held.
|
|
45
|
+
let powerBlockerId = -1;
|
|
43
46
|
|
|
44
47
|
// Boot diagnostics — populated at app.whenReady so the renderer can poll their
|
|
45
48
|
// state via IPC and surface toasts on the failure paths. The first-paint
|
|
@@ -239,6 +242,10 @@ function createWindow() {
|
|
|
239
242
|
contextIsolation: true,
|
|
240
243
|
nodeIntegration: false,
|
|
241
244
|
sandbox: true,
|
|
245
|
+
// Enables Chromium's built-in PDF viewer so the Editor's PdfPane iframe
|
|
246
|
+
// renders .pdf over smfile:// instead of triggering a download. The only
|
|
247
|
+
// Pepper "plugin" in modern Electron is the PDF viewer (Flash is gone).
|
|
248
|
+
plugins: true,
|
|
242
249
|
},
|
|
243
250
|
});
|
|
244
251
|
|
|
@@ -594,6 +601,31 @@ if (process.env.SM_E2E === '1') {
|
|
|
594
601
|
try { app.disableHardwareAcceleration(); } catch { /* */ }
|
|
595
602
|
}
|
|
596
603
|
|
|
604
|
+
// smfile:// — privileged scheme that serves home-scoped files to the in-app
|
|
605
|
+
// Editor's HTML preview iframe. Loading the user's HTML via a custom scheme
|
|
606
|
+
// (rather than srcdoc) gives the iframe document its OWN origin + empty CSP, so
|
|
607
|
+
// the page's own visualization scripts run — while the iframe is still
|
|
608
|
+
// sandboxed without allow-same-origin (opaque origin), keeping it walled off
|
|
609
|
+
// from the host app, its IPC bridge, and the user's filesystem. Must be
|
|
610
|
+
// registered before app.whenReady() (Electron rejects late scheme privileges).
|
|
611
|
+
protocol.registerSchemesAsPrivileged([
|
|
612
|
+
{ scheme: 'smfile', privileges: { standard: true, secure: true, supportFetchAPI: true, stream: true } },
|
|
613
|
+
]);
|
|
614
|
+
|
|
615
|
+
// Common content types for previewed assets. Anything unknown is served as
|
|
616
|
+
// octet-stream so the browser sniffs / downloads rather than mis-rendering.
|
|
617
|
+
const SMFILE_MIME = {
|
|
618
|
+
'.html': 'text/html', '.htm': 'text/html',
|
|
619
|
+
'.css': 'text/css', '.js': 'text/javascript', '.mjs': 'text/javascript',
|
|
620
|
+
'.json': 'application/json', '.svg': 'image/svg+xml',
|
|
621
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
622
|
+
'.gif': 'image/gif', '.webp': 'image/webp', '.ico': 'image/x-icon',
|
|
623
|
+
'.avif': 'image/avif', '.bmp': 'image/bmp',
|
|
624
|
+
'.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf',
|
|
625
|
+
'.txt': 'text/plain', '.csv': 'text/csv', '.xml': 'application/xml',
|
|
626
|
+
'.pdf': 'application/pdf',
|
|
627
|
+
};
|
|
628
|
+
|
|
597
629
|
// Single-instance lock (PRD F1 v2 §requestSingleInstanceLock).
|
|
598
630
|
// In dev mode we skip the lock so two-dev-instance workflows still work.
|
|
599
631
|
// E2E tests also skip so playwright.electron.launch can run multiple specs
|
|
@@ -685,7 +717,7 @@ app.whenReady().then(async () => {
|
|
|
685
717
|
// — adds ~3 packages and ~2MB to the renderer build; the network fetch
|
|
686
718
|
// happens once per cold launch and is cached by Electron's HTTP cache.
|
|
687
719
|
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
|
688
|
-
"img-src 'self' data: blob:",
|
|
720
|
+
"img-src 'self' data: blob: smfile:",
|
|
689
721
|
"font-src 'self' data: https://fonts.gstatic.com",
|
|
690
722
|
// schemastore.org is used by Monaco for JSON schema validation
|
|
691
723
|
// (settings.json, keybindings.json — see App.tsx::installMonacoSchemas).
|
|
@@ -694,10 +726,18 @@ app.whenReady().then(async () => {
|
|
|
694
726
|
"connect-src 'self' https://api.anthropic.com https://registry.npmjs.org https://json.schemastore.org https://www.schemastore.org",
|
|
695
727
|
"media-src 'self' blob:",
|
|
696
728
|
"worker-src 'self' blob:",
|
|
697
|
-
|
|
729
|
+
// smfile: powers the Editor's sandboxed HTML preview iframe. The iframe is
|
|
730
|
+
// sandboxed WITHOUT allow-same-origin, so its document has an opaque origin
|
|
731
|
+
// and cannot reach the host even though it may run its own scripts.
|
|
732
|
+
"frame-src smfile:",
|
|
698
733
|
"frame-ancestors 'none'",
|
|
699
734
|
].join('; ') + ';';
|
|
700
735
|
session.defaultSession.webRequest.onHeadersReceived((details, cb) => {
|
|
736
|
+
// smfile:// serves the Editor's HTML-preview documents. They must NOT
|
|
737
|
+
// inherit the app CSP — `frame-ancestors 'none'` would forbid framing them,
|
|
738
|
+
// and `script-src 'self'` would block the page's own visualization scripts.
|
|
739
|
+
// Isolation comes from the iframe sandbox (opaque origin), not from CSP.
|
|
740
|
+
if (details.url.startsWith('smfile:')) { cb({}); return; }
|
|
701
741
|
cb({
|
|
702
742
|
responseHeaders: {
|
|
703
743
|
...details.responseHeaders,
|
|
@@ -706,6 +746,29 @@ app.whenReady().then(async () => {
|
|
|
706
746
|
});
|
|
707
747
|
});
|
|
708
748
|
|
|
749
|
+
// smfile:// handler — serves a single home-scoped file for the Editor's HTML
|
|
750
|
+
// preview iframe (and its relative assets: ./chart.js, ./data.json, images).
|
|
751
|
+
// URL shape: smfile://local/<absolute-path>. The pathname IS the absolute
|
|
752
|
+
// path; assertInsideHome enforces containment + rejects symlink escapes. The
|
|
753
|
+
// sandboxed iframe's relative requests resolve against this same scheme, so
|
|
754
|
+
// co-located assets load while everything stays inside home.
|
|
755
|
+
protocol.handle('smfile', async (request) => {
|
|
756
|
+
try {
|
|
757
|
+
const url = new URL(request.url);
|
|
758
|
+
const abs = decodeURIComponent(url.pathname);
|
|
759
|
+
const { realPath } = assertInsideHome(abs); // throws if outside home
|
|
760
|
+
const st = await fs.promises.stat(realPath);
|
|
761
|
+
if (st.isDirectory()) return new Response('Not a file', { status: 404 });
|
|
762
|
+
if (st.size > 25 * 1024 * 1024) return new Response('Too large', { status: 413 });
|
|
763
|
+
const buf = await fs.promises.readFile(realPath);
|
|
764
|
+
const ext = path.extname(realPath).toLowerCase();
|
|
765
|
+
const type = SMFILE_MIME[ext] || 'application/octet-stream';
|
|
766
|
+
return new Response(buf, { headers: { 'content-type': type } });
|
|
767
|
+
} catch (err) {
|
|
768
|
+
return new Response(`smfile error: ${err && err.message}`, { status: 404 });
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
|
|
709
772
|
// Grant microphone / media permissions only for trusted origins.
|
|
710
773
|
const MEDIA_PERMS = new Set(['media', 'audioCapture', 'microphone']);
|
|
711
774
|
const isTrustedOrigin = (url) =>
|
|
@@ -792,6 +855,19 @@ app.whenReady().then(async () => {
|
|
|
792
855
|
logs.writeLine({ scope: 'scheduler', level: 'error', message: 'init failed', meta: { error: e?.message } });
|
|
793
856
|
});
|
|
794
857
|
|
|
858
|
+
// Keep the machine awake while the app is open. The scheduler polls billing
|
|
859
|
+
// usage every 2 min and runs `claude -p` jobs that must survive an idle
|
|
860
|
+
// laptop — a system suspend (GNOME/Pop!_OS idle or lid timeout) would freeze
|
|
861
|
+
// both. `prevent-app-suspension` stops suspend but still lets the display
|
|
862
|
+
// dim/sleep, so battery impact is limited to keeping the CPU resumable.
|
|
863
|
+
// On Linux this routes through the org.freedesktop.login1 inhibitor.
|
|
864
|
+
try {
|
|
865
|
+
powerBlockerId = powerSaveBlocker.start('prevent-app-suspension');
|
|
866
|
+
logs.writeLine({ scope: 'main', level: 'info', message: 'powerSaveBlocker started', meta: { id: powerBlockerId } });
|
|
867
|
+
} catch (e) {
|
|
868
|
+
logs.writeLine({ scope: 'main', level: 'warn', message: 'powerSaveBlocker failed', meta: { error: e?.message } });
|
|
869
|
+
}
|
|
870
|
+
|
|
795
871
|
// OTEL: load persisted config and start the exporter only if `enabled`.
|
|
796
872
|
// Failures are non-fatal — the app must keep working without telemetry.
|
|
797
873
|
otelSettings.load()
|
|
@@ -810,6 +886,10 @@ app.on('will-quit', () => {
|
|
|
810
886
|
// PRD F1 v2 §IPC plumbing: must unregisterAll on will-quit.
|
|
811
887
|
try { globalShortcut.unregisterAll(); } catch { /* */ }
|
|
812
888
|
voiceHotkey.disposeOnQuit();
|
|
889
|
+
if (powerBlockerId !== -1) {
|
|
890
|
+
try { powerSaveBlocker.stop(powerBlockerId); } catch { /* */ }
|
|
891
|
+
powerBlockerId = -1;
|
|
892
|
+
}
|
|
813
893
|
});
|
|
814
894
|
|
|
815
895
|
app.on('window-all-closed', () => {
|
package/src/preload/api.d.ts
CHANGED
|
@@ -467,7 +467,7 @@ export interface FileEntry {
|
|
|
467
467
|
}
|
|
468
468
|
|
|
469
469
|
export interface FilesListResult { ok: boolean; entries: FileEntry[]; error: string | null }
|
|
470
|
-
export interface FilesReadResult { ok: boolean; text: string; error: string | null; size: number }
|
|
470
|
+
export interface FilesReadResult { ok: boolean; text: string; error: string | null; size: number; binary?: boolean; mime?: string }
|
|
471
471
|
export interface FilesWriteResult { ok: boolean; error: string | null }
|
|
472
472
|
export interface FilesCreateResult { ok: boolean; path?: string; error: string | null }
|
|
473
473
|
export interface FilesRenameResult { ok: boolean; newPath?: string; error: string | null }
|