claude-code-session-manager 0.8.0 → 0.8.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/dist/assets/{cssMode-CDGOCAW5.js → cssMode-30PYohIN.js} +1 -1
- package/dist/assets/{editor.main-zj3Myqhk.js → editor.main-CZ_l_CSt.js} +3 -3
- package/dist/assets/{freemarker2-Dh6-Vi35.js → freemarker2-DA5xODSz.js} +1 -1
- package/dist/assets/{handlebars-KgXZ3LUu.js → handlebars-BgJKogMf.js} +1 -1
- package/dist/assets/{html-D12q2PkL.js → html-D3DAPwAR.js} +1 -1
- package/dist/assets/{htmlMode-CCaSY5vs.js → htmlMode-mS5mzFjU.js} +1 -1
- package/dist/assets/index-Bs-mHiD-.js +2976 -0
- package/dist/assets/index-DCK87t79.css +32 -0
- package/dist/assets/{javascript-ClkFzW_a.js → javascript-CJ-Uxk_I.js} +1 -1
- package/dist/assets/{jsonMode-BV7azwkW.js → jsonMode-DbcDRati.js} +1 -1
- package/dist/assets/{liquid-C9Id9V-K.js → liquid-I4DHwPR_.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-COD69jzF.js → lspLanguageFeatures-BntDl6Xn.js} +1 -1
- package/dist/assets/{mdx-D8YvWtiq.js → mdx-DWI58irx.js} +1 -1
- package/dist/assets/{python-YtsitwE4.js → python-DPx3c0QA.js} +1 -1
- package/dist/assets/{razor-T48OHh5u.js → razor-BcxFqE_H.js} +1 -1
- package/dist/assets/{tsMode-DHTMR4b8.js → tsMode-CGTi49DJ.js} +1 -1
- package/dist/assets/{typescript-Ckq032Ud.js → typescript-CE9RqBjC.js} +1 -1
- package/dist/assets/{xml-B7dYWEXB.js → xml-DsrLAWcV.js} +1 -1
- package/dist/assets/{yaml-DUufEgrd.js → yaml-CA8rRsQI.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/main/config.cjs +93 -19
- package/src/main/index.cjs +163 -31
- package/src/main/ipcSchemas.cjs +59 -2
- package/src/main/lib/cleanEnv.cjs +20 -0
- package/src/main/lib/credentials.cjs +184 -0
- package/src/main/lib/schedulerConfig.cjs +10 -0
- package/src/main/logs.cjs +1 -1
- package/src/main/otelSettings.cjs +1 -1
- package/src/main/pty.cjs +53 -6
- package/src/main/scheduler.cjs +521 -148
- package/src/main/transcripts.cjs +26 -21
- package/src/main/usage.cjs +76 -25
- package/src/main/voiceSettings.cjs +1 -1
- package/src/main/watchers.cjs +69 -11
- package/src/preload/api.d.ts +53 -11
- package/src/preload/index.cjs +13 -0
- package/dist/assets/index-Dejlz0I1.js +0 -2972
- package/dist/assets/index-DsC4vT8M.css +0 -32
package/src/main/transcripts.cjs
CHANGED
|
@@ -84,40 +84,45 @@ function classifyLine(obj) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
* Read new bytes from
|
|
88
|
-
*
|
|
87
|
+
* Read new bytes from sub.filePath into sub.offset/pending/inode in place.
|
|
88
|
+
* Resets offset+pending when the file inode changes (rename+replace rotation).
|
|
89
|
+
* Returns parsed line strings ready for JSON.parse.
|
|
89
90
|
*/
|
|
90
|
-
async function readDelta(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
async function readDelta(sub) {
|
|
92
|
+
const stat = await fsp.stat(sub.filePath).catch(() => null);
|
|
93
|
+
if (!stat) return [];
|
|
94
|
+
// Inode changed → file was replaced underfoot; restart from the top.
|
|
95
|
+
if (sub.inode !== undefined && stat.ino !== sub.inode) {
|
|
96
|
+
sub.offset = 0;
|
|
97
|
+
sub.pending = '';
|
|
96
98
|
}
|
|
97
|
-
if (stat.size < offset) {
|
|
99
|
+
if (stat.size < sub.offset) {
|
|
98
100
|
// File was truncated/rotated — start over.
|
|
99
|
-
offset = 0;
|
|
100
|
-
pending = '';
|
|
101
|
+
sub.offset = 0;
|
|
102
|
+
sub.pending = '';
|
|
103
|
+
}
|
|
104
|
+
if (stat.size === sub.offset) {
|
|
105
|
+
sub.inode = stat.ino;
|
|
106
|
+
return [];
|
|
101
107
|
}
|
|
102
|
-
|
|
103
|
-
const fd = await fsp.open(filePath, 'r');
|
|
108
|
+
const fd = await fsp.open(sub.filePath, 'r');
|
|
104
109
|
try {
|
|
105
|
-
const length = stat.size - offset;
|
|
110
|
+
const length = stat.size - sub.offset;
|
|
106
111
|
const buf = Buffer.alloc(length);
|
|
107
|
-
await fd.read(buf, 0, length, offset);
|
|
108
|
-
const text = pending + buf.toString('utf8');
|
|
112
|
+
await fd.read(buf, 0, length, sub.offset);
|
|
113
|
+
const text = sub.pending + buf.toString('utf8');
|
|
109
114
|
const parts = text.split('\n');
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
sub.pending = parts.pop() ?? '';
|
|
116
|
+
sub.offset = stat.size;
|
|
117
|
+
sub.inode = stat.ino;
|
|
118
|
+
return parts.filter(Boolean);
|
|
112
119
|
} finally {
|
|
113
120
|
await fd.close();
|
|
114
121
|
}
|
|
115
122
|
}
|
|
116
123
|
|
|
117
124
|
async function flush(sub, { emit = true } = {}) {
|
|
118
|
-
const
|
|
119
|
-
sub.offset = newOffset;
|
|
120
|
-
sub.pending = pending;
|
|
125
|
+
const lines = await readDelta(sub);
|
|
121
126
|
for (const line of lines) {
|
|
122
127
|
let obj;
|
|
123
128
|
try {
|
package/src/main/usage.cjs
CHANGED
|
@@ -20,48 +20,99 @@ const fsp = require('node:fs/promises');
|
|
|
20
20
|
const path = require('node:path');
|
|
21
21
|
const os = require('node:os');
|
|
22
22
|
const { ipcMain } = require('electron');
|
|
23
|
+
const { refreshIfNeeded, expiresAtMs } = require('./lib/credentials.cjs');
|
|
23
24
|
|
|
24
|
-
const CREDS_PATH = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
25
25
|
const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
26
|
+
const CACHE_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'billing-cache.json');
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
let cache = null;
|
|
29
|
+
let hydrationPromise = null;
|
|
30
|
+
|
|
31
|
+
async function hydrateCache() {
|
|
32
|
+
try {
|
|
33
|
+
cache = JSON.parse(await fsp.readFile(CACHE_PATH, 'utf8'));
|
|
34
|
+
} catch {
|
|
35
|
+
cache = null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function persistCache(c) {
|
|
40
|
+
await fsp.mkdir(path.dirname(CACHE_PATH), { recursive: true });
|
|
41
|
+
const tmp = `${CACHE_PATH}.${process.pid}.${Date.now()}.tmp`;
|
|
42
|
+
await fsp.writeFile(tmp, JSON.stringify(c), { encoding: 'utf8', mode: 0o600 });
|
|
43
|
+
await fsp.rename(tmp, CACHE_PATH);
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
async function fetchUsage() {
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
// Check expiry and attempt proactive refresh before touching the network.
|
|
48
|
+
const refresh = await refreshIfNeeded();
|
|
49
|
+
if (refresh.kind === 'auth') {
|
|
50
|
+
return { kind: 'auth', message: refresh.message, httpStatus: 401, expiredAt: refresh.expiredAt ?? null };
|
|
51
|
+
}
|
|
52
|
+
if (refresh.kind === 'config') return refresh;
|
|
53
|
+
// 'ok' or 'unsupported' — creds present and not yet expired
|
|
54
|
+
const creds = refresh.creds;
|
|
55
|
+
|
|
56
|
+
let r;
|
|
57
|
+
try {
|
|
58
|
+
r = await fetch(USAGE_URL, {
|
|
59
|
+
headers: {
|
|
60
|
+
Authorization: `Bearer ${creds.accessToken}`,
|
|
61
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
62
|
+
'User-Agent': 'claude-code-session-manager',
|
|
63
|
+
},
|
|
64
|
+
signal: AbortSignal.timeout(10_000),
|
|
65
|
+
});
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return { kind: 'transient', message: e.message || String(e), httpStatus: null };
|
|
68
|
+
}
|
|
69
|
+
if (r.status === 401 || r.status === 403) {
|
|
70
|
+
const body = await r.text().catch(() => '');
|
|
71
|
+
const ms = expiresAtMs(creds);
|
|
72
|
+
return { kind: 'auth', message: body.slice(0, 200) || `HTTP ${r.status}`, httpStatus: r.status, expiredAt: ms };
|
|
73
|
+
}
|
|
74
|
+
if (r.status === 408 || r.status === 429 || r.status >= 500) {
|
|
75
|
+
const body = await r.text().catch(() => '');
|
|
76
|
+
return { kind: 'transient', message: body.slice(0, 200) || `HTTP ${r.status}`, httpStatus: r.status };
|
|
77
|
+
}
|
|
44
78
|
if (!r.ok) {
|
|
45
79
|
const body = await r.text().catch(() => '');
|
|
46
|
-
|
|
80
|
+
return { kind: 'transient', message: body.slice(0, 200) || `HTTP ${r.status}`, httpStatus: r.status };
|
|
47
81
|
}
|
|
48
82
|
const usage = await r.json();
|
|
49
83
|
return {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
84
|
+
kind: 'ok',
|
|
85
|
+
data: {
|
|
86
|
+
usage,
|
|
87
|
+
subscriptionType: creds.subscriptionType ?? null,
|
|
88
|
+
rateLimitTier: creds.rateLimitTier ?? null,
|
|
89
|
+
credentialsExpiresAt: creds.expiresAt ?? null,
|
|
90
|
+
fetchedAt: Date.now(),
|
|
91
|
+
},
|
|
55
92
|
};
|
|
56
93
|
}
|
|
57
94
|
|
|
58
95
|
function registerBillingHandlers() {
|
|
96
|
+
hydrationPromise = hydrateCache();
|
|
97
|
+
|
|
59
98
|
ipcMain.handle('billing:fetch', async () => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
99
|
+
if (hydrationPromise) { await hydrationPromise; hydrationPromise = null; }
|
|
100
|
+
|
|
101
|
+
const r = await fetchUsage();
|
|
102
|
+
if (r.kind === 'ok') {
|
|
103
|
+
cache = { data: r.data, fetchedAt: Date.now(), sourceCredsExpiresAt: r.data.credentialsExpiresAt };
|
|
104
|
+
persistCache(cache).catch(() => {});
|
|
105
|
+
return { kind: 'ok', data: r.data };
|
|
106
|
+
}
|
|
107
|
+
if (r.kind === 'auth') {
|
|
108
|
+
if (cache) return { kind: 'auth', message: r.message, httpStatus: r.httpStatus, expiredAt: r.expiredAt, cached: cache.data, staleSince: cache.fetchedAt };
|
|
109
|
+
return r;
|
|
110
|
+
}
|
|
111
|
+
if (r.kind === 'transient') {
|
|
112
|
+
if (cache) return { kind: 'ok-stale', data: cache.data, staleSince: cache.fetchedAt, lastError: r.message };
|
|
113
|
+
return r;
|
|
64
114
|
}
|
|
115
|
+
return r; // config
|
|
65
116
|
});
|
|
66
117
|
}
|
|
67
118
|
|
|
@@ -109,7 +109,7 @@ async function writeMerged(patch) {
|
|
|
109
109
|
const next = { ...existing, ...patch };
|
|
110
110
|
const body = JSON.stringify(next, null, 2) + '\n';
|
|
111
111
|
const tmp = `${p}.tmp-${process.pid}-${Date.now()}`;
|
|
112
|
-
await fsp.writeFile(tmp, body, 'utf8',
|
|
112
|
+
await fsp.writeFile(tmp, body, { encoding: 'utf8', mode: 0o600 });
|
|
113
113
|
// chmod tmp explicitly because some platforms ignore the mode arg on
|
|
114
114
|
// writeFile when the file pre-exists. Then rename for atomicity.
|
|
115
115
|
try { await fsp.chmod(tmp, 0o600); } catch { /* */ }
|
package/src/main/watchers.cjs
CHANGED
|
@@ -10,8 +10,40 @@
|
|
|
10
10
|
|
|
11
11
|
const { ipcMain } = require('electron');
|
|
12
12
|
const { spawn } = require('node:child_process');
|
|
13
|
-
const readline = require('node:readline');
|
|
14
13
|
const crypto = require('node:crypto');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const os = require('node:os');
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
const { cleanChildEnv } = require('./lib/cleanEnv.cjs');
|
|
18
|
+
|
|
19
|
+
// Splits a readable stream into lines capped at maxLineBytes. Prevents OOM
|
|
20
|
+
// from commands that produce no newlines (e.g. cat /dev/urandom | base64).
|
|
21
|
+
function lineSplit(stream, onLine, maxLineBytes = 4096) {
|
|
22
|
+
let buf = Buffer.alloc(0);
|
|
23
|
+
stream.on('data', (chunk) => {
|
|
24
|
+
buf = Buffer.concat([buf, chunk]);
|
|
25
|
+
while (true) {
|
|
26
|
+
const nl = buf.indexOf(0x0a);
|
|
27
|
+
if (nl === -1) {
|
|
28
|
+
if (buf.length > maxLineBytes) {
|
|
29
|
+
onLine(buf.slice(0, maxLineBytes).toString('utf8') + '…[truncated]');
|
|
30
|
+
buf = Buffer.alloc(0);
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const line = buf.slice(0, nl).toString('utf8');
|
|
35
|
+
buf = buf.slice(nl + 1);
|
|
36
|
+
onLine(line.length > maxLineBytes ? line.slice(0, maxLineBytes) + '…[truncated]' : line);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
stream.on('end', () => {
|
|
40
|
+
if (buf.length > 0) {
|
|
41
|
+
onLine(buf.length > maxLineBytes
|
|
42
|
+
? buf.slice(0, maxLineBytes).toString('utf8') + '…[truncated]'
|
|
43
|
+
: buf.toString('utf8'));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
15
47
|
|
|
16
48
|
class WatcherManager {
|
|
17
49
|
constructor() {
|
|
@@ -25,13 +57,27 @@ class WatcherManager {
|
|
|
25
57
|
}
|
|
26
58
|
|
|
27
59
|
add({ tabId, label, command, cwd }) {
|
|
60
|
+
const resolvedCwd = cwd || process.cwd();
|
|
61
|
+
|
|
62
|
+
// Require cwd's realpath to be inside homedir.
|
|
63
|
+
const home = os.homedir();
|
|
64
|
+
let realCwd;
|
|
65
|
+
try {
|
|
66
|
+
realCwd = fs.realpathSync(resolvedCwd);
|
|
67
|
+
} catch {
|
|
68
|
+
realCwd = path.resolve(resolvedCwd);
|
|
69
|
+
}
|
|
70
|
+
if (realCwd !== home && !realCwd.startsWith(home + path.sep)) {
|
|
71
|
+
throw new Error(`watcher cwd outside home directory: ${realCwd}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
28
74
|
const watcherId = crypto.randomUUID();
|
|
29
75
|
const trimmedLabel = (label && label.trim()) || command.slice(0, 40);
|
|
30
76
|
const child = spawn(command, [], {
|
|
31
|
-
cwd:
|
|
77
|
+
cwd: resolvedCwd,
|
|
32
78
|
shell: true,
|
|
33
79
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
34
|
-
env:
|
|
80
|
+
env: cleanChildEnv(),
|
|
35
81
|
});
|
|
36
82
|
|
|
37
83
|
const entry = {
|
|
@@ -39,7 +85,7 @@ class WatcherManager {
|
|
|
39
85
|
tabId,
|
|
40
86
|
label: trimmedLabel,
|
|
41
87
|
command,
|
|
42
|
-
cwd:
|
|
88
|
+
cwd: resolvedCwd,
|
|
43
89
|
child,
|
|
44
90
|
startedAt: Date.now(),
|
|
45
91
|
lineCount: 0,
|
|
@@ -59,18 +105,16 @@ class WatcherManager {
|
|
|
59
105
|
}
|
|
60
106
|
};
|
|
61
107
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const stderrRl = readline.createInterface({ input: child.stderr });
|
|
65
|
-
stderrRl.on('line', emitLine);
|
|
108
|
+
lineSplit(child.stdout, emitLine);
|
|
109
|
+
lineSplit(child.stderr, emitLine);
|
|
66
110
|
|
|
67
111
|
child.on('error', (err) => {
|
|
68
112
|
emitLine(`[watcher error] ${err?.message ?? String(err)}`);
|
|
113
|
+
// Remove from map so list() doesn't return ghosts after a spawn failure.
|
|
114
|
+
this.watchers.delete(watcherId);
|
|
69
115
|
});
|
|
70
116
|
|
|
71
117
|
child.on('close', (code, signal) => {
|
|
72
|
-
stdoutRl.close();
|
|
73
|
-
stderrRl.close();
|
|
74
118
|
// Only emit close if still registered (remove() suppresses by deleting first).
|
|
75
119
|
if (this.watchers.has(watcherId)) {
|
|
76
120
|
emitLine(`[exited code=${code ?? 'null'}${signal ? ` signal=${signal}` : ''}]`);
|
|
@@ -135,6 +179,12 @@ class WatcherManager {
|
|
|
135
179
|
|
|
136
180
|
const manager = new WatcherManager();
|
|
137
181
|
|
|
182
|
+
// Top-level export so index.cjs can call watchers.attachWindow(mainWindow)
|
|
183
|
+
// without going through the manager instance directly.
|
|
184
|
+
function attachWindow(window) {
|
|
185
|
+
manager.attachWindow(window);
|
|
186
|
+
}
|
|
187
|
+
|
|
138
188
|
function registerWatcherHandlers() {
|
|
139
189
|
const { z } = require('zod');
|
|
140
190
|
const addSchema = z.object({
|
|
@@ -145,10 +195,18 @@ function registerWatcherHandlers() {
|
|
|
145
195
|
});
|
|
146
196
|
const listSchema = z.object({ tabId: z.string().min(1).max(128) });
|
|
147
197
|
const removeSchema = z.object({ watcherId: z.string().min(1).max(128) });
|
|
198
|
+
const killTabSchema = z.object({ tabId: z.string().min(1).max(128) });
|
|
148
199
|
|
|
149
200
|
ipcMain.handle('watchers:add', (_e, payload) => manager.add(addSchema.parse(payload)));
|
|
150
201
|
ipcMain.handle('watchers:list', (_e, payload) => manager.list(listSchema.parse(payload)));
|
|
151
202
|
ipcMain.handle('watchers:remove', (_e, payload) => manager.remove(removeSchema.parse(payload)));
|
|
203
|
+
ipcMain.handle('watchers:kill-tab', (_e, payload) => {
|
|
204
|
+
const { tabId } = killTabSchema.parse(payload);
|
|
205
|
+
for (const [id, w] of manager.watchers) {
|
|
206
|
+
if (w.tabId === tabId) manager.remove({ watcherId: id });
|
|
207
|
+
}
|
|
208
|
+
return { ok: true };
|
|
209
|
+
});
|
|
152
210
|
}
|
|
153
211
|
|
|
154
|
-
module.exports = { manager, registerWatcherHandlers };
|
|
212
|
+
module.exports = { manager, attachWindow, registerWatcherHandlers };
|
package/src/preload/api.d.ts
CHANGED
|
@@ -10,6 +10,11 @@ export interface PtyExit {
|
|
|
10
10
|
signal?: number;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export interface WriteErrorEvent {
|
|
14
|
+
tabId: string;
|
|
15
|
+
reason: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
export interface ReadJsonResult {
|
|
14
19
|
exists: boolean;
|
|
15
20
|
raw: string;
|
|
@@ -84,6 +89,8 @@ export interface PersistedTab {
|
|
|
84
89
|
export interface LoadedSessions {
|
|
85
90
|
tabs: PersistedTab[];
|
|
86
91
|
activeTabId: string | null;
|
|
92
|
+
/** True when the main process rotated all session IDs on boot (e.g. force-fresh). */
|
|
93
|
+
freshStart?: boolean;
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
export interface UsageWindow {
|
|
@@ -107,18 +114,21 @@ export interface UsageSnapshot {
|
|
|
107
114
|
[key: string]: unknown;
|
|
108
115
|
}
|
|
109
116
|
|
|
110
|
-
export interface
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
credentialsExpiresAt: number | null;
|
|
117
|
-
fetchedAt: number;
|
|
118
|
-
};
|
|
119
|
-
error?: string;
|
|
117
|
+
export interface BillingData {
|
|
118
|
+
usage: UsageSnapshot;
|
|
119
|
+
subscriptionType: string | null;
|
|
120
|
+
rateLimitTier: string | null;
|
|
121
|
+
credentialsExpiresAt: string | null;
|
|
122
|
+
fetchedAt: number;
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
export type BillingFetchResult =
|
|
126
|
+
| { kind: 'ok'; data: BillingData }
|
|
127
|
+
| { kind: 'ok-stale'; data: BillingData; staleSince: number; lastError: string }
|
|
128
|
+
| { kind: 'auth'; message: string; httpStatus: number; expiredAt?: number | null; cached?: BillingData; staleSince?: number }
|
|
129
|
+
| { kind: 'transient'; message: string; httpStatus: number | null }
|
|
130
|
+
| { kind: 'config'; message: string };
|
|
131
|
+
|
|
122
132
|
export interface VoiceHotkeyConfig {
|
|
123
133
|
accelerator: string;
|
|
124
134
|
mode: 'hold' | 'toggle';
|
|
@@ -259,7 +269,7 @@ export interface SchedulePaths {
|
|
|
259
269
|
queue: string;
|
|
260
270
|
}
|
|
261
271
|
|
|
262
|
-
export type SchedulePauseReason = 'rate_limit';
|
|
272
|
+
export type SchedulePauseReason = 'rate_limit' | 'auth' | 'network' | 'manual' | 'reset_failure';
|
|
263
273
|
|
|
264
274
|
export interface SchedulePauseInfo {
|
|
265
275
|
reason: SchedulePauseReason;
|
|
@@ -270,6 +280,27 @@ export interface SchedulePauseInfo {
|
|
|
270
280
|
resumeAt: string | null;
|
|
271
281
|
}
|
|
272
282
|
|
|
283
|
+
export interface ScheduleHealthSnapshot {
|
|
284
|
+
bootedAt: number;
|
|
285
|
+
lastPollAt: number | null;
|
|
286
|
+
lastPollOk: boolean;
|
|
287
|
+
consecutiveFailures: number;
|
|
288
|
+
backoffNextAt: number | null;
|
|
289
|
+
nextResetCached: string | null;
|
|
290
|
+
pausedSince: number | null;
|
|
291
|
+
pauseReason: SchedulePauseReason | null;
|
|
292
|
+
runningJobs: { slug: string; startedAt: number; pid: number }[];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export interface PrdListItem {
|
|
296
|
+
slug: string;
|
|
297
|
+
parallelGroup: number;
|
|
298
|
+
title: string;
|
|
299
|
+
cwd: string;
|
|
300
|
+
estimateMinutes: number | null;
|
|
301
|
+
mtimeMs: number;
|
|
302
|
+
}
|
|
303
|
+
|
|
273
304
|
export interface ScheduleStateSnapshot {
|
|
274
305
|
config: ScheduleConfig;
|
|
275
306
|
jobs: ScheduleJob[];
|
|
@@ -331,6 +362,10 @@ export interface SessionManagerAPI {
|
|
|
331
362
|
isE2E: () => Promise<boolean>;
|
|
332
363
|
onNewSession: (handler: () => void) => () => void;
|
|
333
364
|
onRebootSession: (handler: () => void) => () => void;
|
|
365
|
+
openInEditor: (cwd: string, editor?: string | null) => Promise<{ ok: boolean; editor?: string; error?: string }>;
|
|
366
|
+
openInFinder: (cwd: string) => Promise<{ ok: boolean; error?: string }>;
|
|
367
|
+
openInTerminal: (cwd: string) => Promise<{ ok: boolean; terminal?: string; error?: string }>;
|
|
368
|
+
archiveProject: (encoded: string) => Promise<{ ok: boolean; error?: string }>;
|
|
334
369
|
};
|
|
335
370
|
pty: {
|
|
336
371
|
spawn: (payload: { tabId: string; cwd: string; cols?: number; rows?: number }) => Promise<SpawnResult>;
|
|
@@ -339,6 +374,7 @@ export interface SessionManagerAPI {
|
|
|
339
374
|
kill: (tabId: string) => void;
|
|
340
375
|
onData: (tabId: string, handler: (data: string) => void) => () => void;
|
|
341
376
|
onExit: (tabId: string, handler: (info: PtyExit) => void) => () => void;
|
|
377
|
+
onWriteError: (handler: (ev: WriteErrorEvent) => void) => () => void;
|
|
342
378
|
};
|
|
343
379
|
transcripts: {
|
|
344
380
|
subscribe: (payload: { tabId: string; cwd: string; sessionUuid: string }) => Promise<SubscribeResult>;
|
|
@@ -393,6 +429,7 @@ export interface SessionManagerAPI {
|
|
|
393
429
|
add: (payload: { tabId: string; label?: string; command: string; cwd?: string | null }) => Promise<WatcherAddResult>;
|
|
394
430
|
list: (tabId: string) => Promise<WatcherInfo[]>;
|
|
395
431
|
remove: (watcherId: string) => Promise<{ ok: boolean }>;
|
|
432
|
+
killTab: (tabId: string) => Promise<{ ok: boolean }>;
|
|
396
433
|
onLine: (handler: (ev: WatcherLineEvent) => void) => () => void;
|
|
397
434
|
onClosed: (handler: (ev: WatcherClosedEvent) => void) => () => void;
|
|
398
435
|
};
|
|
@@ -412,6 +449,9 @@ export interface SessionManagerAPI {
|
|
|
412
449
|
openFolder: () => Promise<{ ok: boolean }>;
|
|
413
450
|
readPrd: (slug: string) => Promise<{ ok: boolean; text?: string; error?: string }>;
|
|
414
451
|
readLog: (runId: string, slug: string) => Promise<{ ok: boolean; text?: string; error?: string }>;
|
|
452
|
+
writePrd: (slug: string, body: string) => Promise<{ ok: boolean; bytesWritten: number }>;
|
|
453
|
+
listPrds: () => Promise<PrdListItem[]>;
|
|
454
|
+
health: () => Promise<ScheduleHealthSnapshot>;
|
|
415
455
|
onState: (handler: (snapshot: ScheduleStateSnapshot) => void) => () => void;
|
|
416
456
|
};
|
|
417
457
|
}
|
|
@@ -420,6 +460,8 @@ declare global {
|
|
|
420
460
|
interface Window {
|
|
421
461
|
api: SessionManagerAPI;
|
|
422
462
|
}
|
|
463
|
+
// Injected by Vite's `define` from package.json at build time.
|
|
464
|
+
const __APP_VERSION__: string;
|
|
423
465
|
}
|
|
424
466
|
|
|
425
467
|
export {};
|
package/src/preload/index.cjs
CHANGED
|
@@ -9,6 +9,10 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
9
9
|
pickDirectory: () => ipcRenderer.invoke('app:pick-directory'),
|
|
10
10
|
gitBranch: (cwd) => ipcRenderer.invoke('app:git-branch', { cwd }),
|
|
11
11
|
rebootApp: () => ipcRenderer.send('app:reboot-app'),
|
|
12
|
+
openInEditor: (cwd, editor) => ipcRenderer.invoke('app:open-in-editor', { cwd, editor }),
|
|
13
|
+
openInFinder: (cwd) => ipcRenderer.invoke('app:open-in-finder', { cwd }),
|
|
14
|
+
openInTerminal: (cwd) => ipcRenderer.invoke('app:open-in-terminal', { cwd }),
|
|
15
|
+
archiveProject: (encoded) => ipcRenderer.invoke('app:archive-project', { encoded }),
|
|
12
16
|
testFireHook: (args) => ipcRenderer.invoke('app:test-fire-hook', args),
|
|
13
17
|
// F7: lets the renderer suppress the wizard auto-trigger under SM_E2E=1.
|
|
14
18
|
isE2E: () => ipcRenderer.invoke('app:is-e2e'),
|
|
@@ -40,6 +44,11 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
40
44
|
ipcRenderer.on(channel, listener);
|
|
41
45
|
return () => ipcRenderer.removeListener(channel, listener);
|
|
42
46
|
},
|
|
47
|
+
onWriteError: (handler) => {
|
|
48
|
+
const listener = (_e, payload) => handler(payload);
|
|
49
|
+
ipcRenderer.on('pty:write-error', listener);
|
|
50
|
+
return () => ipcRenderer.removeListener('pty:write-error', listener);
|
|
51
|
+
},
|
|
43
52
|
},
|
|
44
53
|
transcripts: {
|
|
45
54
|
subscribe: (payload) => ipcRenderer.invoke('transcript:subscribe', payload),
|
|
@@ -111,6 +120,7 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
111
120
|
add: (payload) => ipcRenderer.invoke('watchers:add', payload),
|
|
112
121
|
list: (tabId) => ipcRenderer.invoke('watchers:list', { tabId }),
|
|
113
122
|
remove: (watcherId) => ipcRenderer.invoke('watchers:remove', { watcherId }),
|
|
123
|
+
killTab: (tabId) => ipcRenderer.invoke('watchers:kill-tab', { tabId }),
|
|
114
124
|
onLine: (handler) => {
|
|
115
125
|
const listener = (_e, payload) => handler(payload);
|
|
116
126
|
ipcRenderer.on('watcher:line', listener);
|
|
@@ -138,6 +148,9 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
138
148
|
openFolder: () => ipcRenderer.invoke('schedule:open-folder'),
|
|
139
149
|
readPrd: (slug) => ipcRenderer.invoke('schedule:read-prd', { slug }),
|
|
140
150
|
readLog: (runId, slug) => ipcRenderer.invoke('schedule:read-log', { runId, slug }),
|
|
151
|
+
writePrd: (slug, body) => ipcRenderer.invoke('schedule:write-prd', { slug, body }),
|
|
152
|
+
listPrds: () => ipcRenderer.invoke('schedule:list-prds'),
|
|
153
|
+
health: () => ipcRenderer.invoke('schedule:health'),
|
|
141
154
|
onState: (handler) => {
|
|
142
155
|
const listener = (_e, payload) => handler(payload);
|
|
143
156
|
ipcRenderer.on('schedule:state', listener);
|