terminalos 0.4.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/README.md +324 -0
- package/bin/cli.js +28 -0
- package/build/assets/html2pdf-S6WA7sS3.js +240 -0
- package/build/assets/index-CfXPiaFw.css +32 -0
- package/build/assets/index-DfoqUTmD.js +141 -0
- package/build/index.html +17 -0
- package/electron/fs-watcher.js +181 -0
- package/electron/fs-watcher.ts +183 -0
- package/electron/main.js +245 -0
- package/electron/main.ts +241 -0
- package/electron/preload.js +77 -0
- package/electron/preload.ts +105 -0
- package/electron/process-detector.js +63 -0
- package/electron/process-detector.ts +70 -0
- package/electron/pty-manager.js +188 -0
- package/electron/pty-manager.ts +181 -0
- package/electron/tsconfig.json +14 -0
- package/electron/versions-manager.js +74 -0
- package/electron/versions-manager.ts +98 -0
- package/electron/window-state.js +49 -0
- package/electron/window-state.ts +52 -0
- package/package.json +104 -0
- package/runtime-dist/server.js +626 -0
package/electron/main.ts
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { app, BrowserWindow, ipcMain, dialog, shell, protocol, net } from 'electron'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { WindowState } from './window-state'
|
|
4
|
+
import { PtyManager } from './pty-manager'
|
|
5
|
+
import { FsWatcher } from './fs-watcher'
|
|
6
|
+
import { VersionsManager } from './versions-manager'
|
|
7
|
+
|
|
8
|
+
// Must be called before app.ready
|
|
9
|
+
protocol.registerSchemesAsPrivileged([
|
|
10
|
+
{ scheme: 'localfile', privileges: { secure: true, supportFetchAPI: true, stream: true } },
|
|
11
|
+
])
|
|
12
|
+
|
|
13
|
+
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged
|
|
14
|
+
|
|
15
|
+
let mainWindow: BrowserWindow | null = null
|
|
16
|
+
let ptyManager: PtyManager
|
|
17
|
+
let fsWatcher: FsWatcher
|
|
18
|
+
let versionsManager: VersionsManager
|
|
19
|
+
|
|
20
|
+
function semverGt(a: string, b: string): boolean {
|
|
21
|
+
const pa = a.split('.').map(Number)
|
|
22
|
+
const pb = b.split('.').map(Number)
|
|
23
|
+
for (let i = 0; i < 3; i++) {
|
|
24
|
+
if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true
|
|
25
|
+
if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false
|
|
26
|
+
}
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createWindow() {
|
|
31
|
+
protocol.handle('localfile', (request) => {
|
|
32
|
+
const filePath = new URL(request.url).pathname
|
|
33
|
+
return net.fetch('file://' + filePath)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const windowState = new WindowState()
|
|
37
|
+
const bounds = windowState.get()
|
|
38
|
+
|
|
39
|
+
mainWindow = new BrowserWindow({
|
|
40
|
+
x: bounds.x,
|
|
41
|
+
y: bounds.y,
|
|
42
|
+
width: bounds.width,
|
|
43
|
+
height: bounds.height,
|
|
44
|
+
minWidth: 640,
|
|
45
|
+
minHeight: 400,
|
|
46
|
+
frame: false,
|
|
47
|
+
titleBarStyle: 'hidden',
|
|
48
|
+
titleBarOverlay: false,
|
|
49
|
+
trafficLightPosition: { x: 12, y: 9 },
|
|
50
|
+
backgroundColor: '#090909',
|
|
51
|
+
show: false,
|
|
52
|
+
webPreferences: {
|
|
53
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
54
|
+
contextIsolation: true,
|
|
55
|
+
nodeIntegration: false,
|
|
56
|
+
sandbox: false,
|
|
57
|
+
spellcheck: false,
|
|
58
|
+
backgroundThrottling: false,
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
ptyManager = new PtyManager(mainWindow)
|
|
63
|
+
fsWatcher = new FsWatcher(mainWindow)
|
|
64
|
+
versionsManager = new VersionsManager()
|
|
65
|
+
|
|
66
|
+
if (isDev) {
|
|
67
|
+
mainWindow.loadURL('http://localhost:5173')
|
|
68
|
+
mainWindow.webContents.openDevTools()
|
|
69
|
+
} else {
|
|
70
|
+
mainWindow.loadFile(path.join(__dirname, '../build/index.html'))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
mainWindow.once('ready-to-show', () => {
|
|
74
|
+
mainWindow?.show()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
mainWindow.on('resize', () => {
|
|
78
|
+
windowState.save(mainWindow!)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
mainWindow.on('move', () => {
|
|
82
|
+
windowState.save(mainWindow!)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
mainWindow.on('closed', () => {
|
|
86
|
+
mainWindow = null
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
setupIpcHandlers()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function setupIpcHandlers() {
|
|
93
|
+
// PTY handlers
|
|
94
|
+
ipcMain.handle('pty:create', async (_, opts: { cwd?: string; env?: Record<string, string> }) => {
|
|
95
|
+
return ptyManager.create(opts)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
ipcMain.on('pty:write', (_, sessionId: string, data: string) => {
|
|
99
|
+
ptyManager.write(sessionId, data)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
ipcMain.on('pty:resize', (_, sessionId: string, cols: number, rows: number) => {
|
|
103
|
+
ptyManager.resize(sessionId, cols, rows)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
ipcMain.handle('pty:kill', async (_, sessionId: string) => {
|
|
107
|
+
return ptyManager.kill(sessionId)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// FS handlers
|
|
111
|
+
ipcMain.handle('fs:openFolder', async () => {
|
|
112
|
+
const result = await dialog.showOpenDialog(mainWindow!, {
|
|
113
|
+
properties: ['openDirectory'],
|
|
114
|
+
})
|
|
115
|
+
if (result.canceled || result.filePaths.length === 0) return null
|
|
116
|
+
return result.filePaths[0]
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
ipcMain.handle('fs:readDir', async (_, dirPath: string) => {
|
|
120
|
+
return fsWatcher.readDir(dirPath)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
ipcMain.handle('fs:readFile', async (_, filePath: string) => {
|
|
124
|
+
return fsWatcher.readFile(filePath)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
ipcMain.handle('fs:writeFile', async (_, filePath: string, content: string) => {
|
|
128
|
+
return fsWatcher.writeFile(filePath, content)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
ipcMain.handle('fs:mkdir', async (_, dirPath: string) => {
|
|
132
|
+
return fsWatcher.mkdir(dirPath)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
ipcMain.handle('fs:rename', async (_, src: string, dest: string) => {
|
|
136
|
+
return fsWatcher.rename(src, dest)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
ipcMain.handle('fs:copyExternal', async (_, srcPath: string, destDir: string) => {
|
|
140
|
+
return fsWatcher.copyExternal(srcPath, destDir)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
ipcMain.handle('fs:delete', async (_, targetPath: string) => {
|
|
144
|
+
return fsWatcher.delete(targetPath)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
ipcMain.handle('fs:writeBinaryFile', async (_, filePath: string, data: ArrayBuffer) => {
|
|
148
|
+
const { promises: fs } = await import('fs')
|
|
149
|
+
await fs.writeFile(filePath, Buffer.from(data))
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
ipcMain.on('fs:setWatchRoot', (_, rootPath: string) => {
|
|
153
|
+
fsWatcher.setWatchRoot(rootPath)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// App handlers
|
|
157
|
+
ipcMain.handle('app:getVersion', async () => {
|
|
158
|
+
return app.getVersion()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
ipcMain.handle('app:getGitBranch', async (_, cwd: string) => {
|
|
162
|
+
const { execSync } = await import('child_process')
|
|
163
|
+
try {
|
|
164
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd, encoding: 'utf8' }).trim()
|
|
165
|
+
return branch
|
|
166
|
+
} catch {
|
|
167
|
+
return null
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
ipcMain.handle('app:checkForUpdates', async () => {
|
|
172
|
+
try {
|
|
173
|
+
const res = await net.fetch('https://api.github.com/repos/vbfs/terminalOS/releases/latest', {
|
|
174
|
+
headers: { 'User-Agent': 'aiTerm-updater' },
|
|
175
|
+
})
|
|
176
|
+
if (!res.ok) return null
|
|
177
|
+
const data = await res.json() as { tag_name: string; html_url: string }
|
|
178
|
+
const latest = data.tag_name.replace(/^v/, '')
|
|
179
|
+
const current = app.getVersion()
|
|
180
|
+
return semverGt(latest, current) ? { version: latest, url: data.html_url } : null
|
|
181
|
+
} catch {
|
|
182
|
+
return null
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// Window controls
|
|
187
|
+
ipcMain.on('window:minimize', () => mainWindow?.minimize())
|
|
188
|
+
ipcMain.on('window:maximize', () => {
|
|
189
|
+
if (mainWindow?.isMaximized()) {
|
|
190
|
+
mainWindow.unmaximize()
|
|
191
|
+
} else {
|
|
192
|
+
mainWindow?.maximize()
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
ipcMain.on('window:close', () => mainWindow?.close())
|
|
196
|
+
|
|
197
|
+
// Shell operations
|
|
198
|
+
ipcMain.on('shell:openPath', (_, filePath: string) => {
|
|
199
|
+
shell.showItemInFolder(filePath)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
ipcMain.on('shell:openInFinder', (_, folderPath: string) => {
|
|
203
|
+
shell.openPath(folderPath)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
ipcMain.on('shell:openExternal', (_, url: string) => {
|
|
207
|
+
shell.openExternal(url)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
// Version history handlers
|
|
211
|
+
ipcMain.handle('fs:versions:save', async (_, filePath: string, content: string) => {
|
|
212
|
+
return versionsManager.saveVersion(filePath, content)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
ipcMain.handle('fs:versions:list', async (_, filePath: string) => {
|
|
216
|
+
return versionsManager.listVersions(filePath)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
ipcMain.handle('fs:versions:get', async (_, filePath: string, versionId: string) => {
|
|
220
|
+
return versionsManager.getVersion(filePath, versionId)
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
app.whenReady().then(createWindow)
|
|
225
|
+
|
|
226
|
+
app.on('before-quit', () => {
|
|
227
|
+
ptyManager?.killAll()
|
|
228
|
+
fsWatcher?.close()
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
app.on('window-all-closed', () => {
|
|
232
|
+
if (process.platform !== 'darwin') {
|
|
233
|
+
app.quit()
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
app.on('activate', () => {
|
|
238
|
+
if (BrowserWindow.getAllWindows().length === 0) {
|
|
239
|
+
createWindow()
|
|
240
|
+
}
|
|
241
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const electron_1 = require("electron");
|
|
4
|
+
const api = {
|
|
5
|
+
pty: {
|
|
6
|
+
create: (opts) => electron_1.ipcRenderer.invoke('pty:create', opts),
|
|
7
|
+
write: (sessionId, data) => electron_1.ipcRenderer.send('pty:write', sessionId, data),
|
|
8
|
+
resize: (sessionId, cols, rows) => electron_1.ipcRenderer.send('pty:resize', sessionId, cols, rows),
|
|
9
|
+
kill: (sessionId) => electron_1.ipcRenderer.invoke('pty:kill', sessionId),
|
|
10
|
+
onData: (cb) => {
|
|
11
|
+
const handler = (_, sessionId, data) => cb(sessionId, data);
|
|
12
|
+
electron_1.ipcRenderer.on('pty:data', handler);
|
|
13
|
+
return () => electron_1.ipcRenderer.removeListener('pty:data', handler);
|
|
14
|
+
},
|
|
15
|
+
onExit: (cb) => {
|
|
16
|
+
const handler = (_, sessionId, code) => cb(sessionId, code);
|
|
17
|
+
electron_1.ipcRenderer.on('pty:exit', handler);
|
|
18
|
+
return () => electron_1.ipcRenderer.removeListener('pty:exit', handler);
|
|
19
|
+
},
|
|
20
|
+
onAiDetected: (cb) => {
|
|
21
|
+
const handler = (_, sessionId, aiProcess) => cb(sessionId, aiProcess);
|
|
22
|
+
electron_1.ipcRenderer.on('pty:ai-detected', handler);
|
|
23
|
+
return () => electron_1.ipcRenderer.removeListener('pty:ai-detected', handler);
|
|
24
|
+
},
|
|
25
|
+
onAiExited: (cb) => {
|
|
26
|
+
const handler = (_, sessionId) => cb(sessionId);
|
|
27
|
+
electron_1.ipcRenderer.on('pty:ai-exited', handler);
|
|
28
|
+
return () => electron_1.ipcRenderer.removeListener('pty:ai-exited', handler);
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
fs: {
|
|
32
|
+
openFolder: () => electron_1.ipcRenderer.invoke('fs:openFolder'),
|
|
33
|
+
readDir: (dirPath) => electron_1.ipcRenderer.invoke('fs:readDir', dirPath),
|
|
34
|
+
readFile: (filePath) => electron_1.ipcRenderer.invoke('fs:readFile', filePath),
|
|
35
|
+
writeFile: (filePath, content) => electron_1.ipcRenderer.invoke('fs:writeFile', filePath, content),
|
|
36
|
+
writeBinaryFile: (filePath, data) => electron_1.ipcRenderer.invoke('fs:writeBinaryFile', filePath, data),
|
|
37
|
+
mkdir: (dirPath) => electron_1.ipcRenderer.invoke('fs:mkdir', dirPath),
|
|
38
|
+
rename: (src, dest) => electron_1.ipcRenderer.invoke('fs:rename', src, dest),
|
|
39
|
+
copyExternal: (srcPath, destDir) => electron_1.ipcRenderer.invoke('fs:copyExternal', srcPath, destDir),
|
|
40
|
+
delete: (targetPath) => electron_1.ipcRenderer.invoke('fs:delete', targetPath),
|
|
41
|
+
setWatchRoot: (rootPath) => electron_1.ipcRenderer.send('fs:setWatchRoot', rootPath),
|
|
42
|
+
onWatch: (cb) => {
|
|
43
|
+
const handler = (_, event) => cb(event);
|
|
44
|
+
electron_1.ipcRenderer.on('fs:watch', handler);
|
|
45
|
+
return () => electron_1.ipcRenderer.removeListener('fs:watch', handler);
|
|
46
|
+
},
|
|
47
|
+
versions: {
|
|
48
|
+
save: (filePath, content) => electron_1.ipcRenderer.invoke('fs:versions:save', filePath, content),
|
|
49
|
+
list: (filePath) => electron_1.ipcRenderer.invoke('fs:versions:list', filePath),
|
|
50
|
+
get: (filePath, versionId) => electron_1.ipcRenderer.invoke('fs:versions:get', filePath, versionId),
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
app: {
|
|
54
|
+
getVersion: () => electron_1.ipcRenderer.invoke('app:getVersion'),
|
|
55
|
+
getGitBranch: (cwd) => electron_1.ipcRenderer.invoke('app:getGitBranch', cwd),
|
|
56
|
+
checkForUpdates: () => electron_1.ipcRenderer.invoke('app:checkForUpdates'),
|
|
57
|
+
onFocus: (cb) => {
|
|
58
|
+
electron_1.ipcRenderer.on('app:focus', cb);
|
|
59
|
+
return () => electron_1.ipcRenderer.removeListener('app:focus', cb);
|
|
60
|
+
},
|
|
61
|
+
onBlur: (cb) => {
|
|
62
|
+
electron_1.ipcRenderer.on('app:blur', cb);
|
|
63
|
+
return () => electron_1.ipcRenderer.removeListener('app:blur', cb);
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
window: {
|
|
67
|
+
minimize: () => electron_1.ipcRenderer.send('window:minimize'),
|
|
68
|
+
maximize: () => electron_1.ipcRenderer.send('window:maximize'),
|
|
69
|
+
close: () => electron_1.ipcRenderer.send('window:close'),
|
|
70
|
+
},
|
|
71
|
+
shell: {
|
|
72
|
+
openPath: (filePath) => electron_1.ipcRenderer.send('shell:openPath', filePath),
|
|
73
|
+
openInFinder: (folderPath) => electron_1.ipcRenderer.send('shell:openInFinder', folderPath),
|
|
74
|
+
openExternal: (url) => electron_1.ipcRenderer.send('shell:openExternal', url),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
electron_1.contextBridge.exposeInMainWorld('api', api);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { contextBridge, ipcRenderer } from 'electron'
|
|
2
|
+
|
|
3
|
+
type Unsubscribe = () => void
|
|
4
|
+
|
|
5
|
+
const api = {
|
|
6
|
+
pty: {
|
|
7
|
+
create: (opts: { cwd?: string; env?: Record<string, string> }): Promise<string> =>
|
|
8
|
+
ipcRenderer.invoke('pty:create', opts),
|
|
9
|
+
write: (sessionId: string, data: string): void =>
|
|
10
|
+
ipcRenderer.send('pty:write', sessionId, data),
|
|
11
|
+
resize: (sessionId: string, cols: number, rows: number): void =>
|
|
12
|
+
ipcRenderer.send('pty:resize', sessionId, cols, rows),
|
|
13
|
+
kill: (sessionId: string): Promise<void> =>
|
|
14
|
+
ipcRenderer.invoke('pty:kill', sessionId),
|
|
15
|
+
onData: (cb: (sessionId: string, data: string) => void): Unsubscribe => {
|
|
16
|
+
const handler = (_: Electron.IpcRendererEvent, sessionId: string, data: string) =>
|
|
17
|
+
cb(sessionId, data)
|
|
18
|
+
ipcRenderer.on('pty:data', handler)
|
|
19
|
+
return () => ipcRenderer.removeListener('pty:data', handler)
|
|
20
|
+
},
|
|
21
|
+
onExit: (cb: (sessionId: string, code: number) => void): Unsubscribe => {
|
|
22
|
+
const handler = (_: Electron.IpcRendererEvent, sessionId: string, code: number) =>
|
|
23
|
+
cb(sessionId, code)
|
|
24
|
+
ipcRenderer.on('pty:exit', handler)
|
|
25
|
+
return () => ipcRenderer.removeListener('pty:exit', handler)
|
|
26
|
+
},
|
|
27
|
+
onAiDetected: (cb: (sessionId: string, aiProcess: { name: string; color: string }) => void): Unsubscribe => {
|
|
28
|
+
const handler = (_: Electron.IpcRendererEvent, sessionId: string, aiProcess: { name: string; color: string }) =>
|
|
29
|
+
cb(sessionId, aiProcess)
|
|
30
|
+
ipcRenderer.on('pty:ai-detected', handler)
|
|
31
|
+
return () => ipcRenderer.removeListener('pty:ai-detected', handler)
|
|
32
|
+
},
|
|
33
|
+
onAiExited: (cb: (sessionId: string) => void): Unsubscribe => {
|
|
34
|
+
const handler = (_: Electron.IpcRendererEvent, sessionId: string) => cb(sessionId)
|
|
35
|
+
ipcRenderer.on('pty:ai-exited', handler)
|
|
36
|
+
return () => ipcRenderer.removeListener('pty:ai-exited', handler)
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
fs: {
|
|
40
|
+
openFolder: (): Promise<string | null> =>
|
|
41
|
+
ipcRenderer.invoke('fs:openFolder'),
|
|
42
|
+
readDir: (dirPath: string): Promise<Array<{ name: string; path: string; isDirectory: boolean; ext: string; size?: number }>> =>
|
|
43
|
+
ipcRenderer.invoke('fs:readDir', dirPath),
|
|
44
|
+
readFile: (filePath: string): Promise<string> =>
|
|
45
|
+
ipcRenderer.invoke('fs:readFile', filePath),
|
|
46
|
+
writeFile: (filePath: string, content: string): Promise<void> =>
|
|
47
|
+
ipcRenderer.invoke('fs:writeFile', filePath, content),
|
|
48
|
+
writeBinaryFile: (filePath: string, data: ArrayBuffer): Promise<void> =>
|
|
49
|
+
ipcRenderer.invoke('fs:writeBinaryFile', filePath, data),
|
|
50
|
+
mkdir: (dirPath: string): Promise<void> =>
|
|
51
|
+
ipcRenderer.invoke('fs:mkdir', dirPath),
|
|
52
|
+
rename: (src: string, dest: string): Promise<void> =>
|
|
53
|
+
ipcRenderer.invoke('fs:rename', src, dest),
|
|
54
|
+
copyExternal: (srcPath: string, destDir: string): Promise<void> =>
|
|
55
|
+
ipcRenderer.invoke('fs:copyExternal', srcPath, destDir),
|
|
56
|
+
delete: (targetPath: string): Promise<void> =>
|
|
57
|
+
ipcRenderer.invoke('fs:delete', targetPath),
|
|
58
|
+
setWatchRoot: (rootPath: string): void =>
|
|
59
|
+
ipcRenderer.send('fs:setWatchRoot', rootPath),
|
|
60
|
+
onWatch: (cb: (event: { type: string; path: string }) => void): Unsubscribe => {
|
|
61
|
+
const handler = (_: Electron.IpcRendererEvent, event: { type: string; path: string }) =>
|
|
62
|
+
cb(event)
|
|
63
|
+
ipcRenderer.on('fs:watch', handler)
|
|
64
|
+
return () => ipcRenderer.removeListener('fs:watch', handler)
|
|
65
|
+
},
|
|
66
|
+
versions: {
|
|
67
|
+
save: (filePath: string, content: string) =>
|
|
68
|
+
ipcRenderer.invoke('fs:versions:save', filePath, content),
|
|
69
|
+
list: (filePath: string) =>
|
|
70
|
+
ipcRenderer.invoke('fs:versions:list', filePath),
|
|
71
|
+
get: (filePath: string, versionId: string) =>
|
|
72
|
+
ipcRenderer.invoke('fs:versions:get', filePath, versionId),
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
app: {
|
|
76
|
+
getVersion: (): Promise<string> =>
|
|
77
|
+
ipcRenderer.invoke('app:getVersion'),
|
|
78
|
+
getGitBranch: (cwd: string): Promise<string | null> =>
|
|
79
|
+
ipcRenderer.invoke('app:getGitBranch', cwd),
|
|
80
|
+
checkForUpdates: (): Promise<{ version: string; url: string } | null> =>
|
|
81
|
+
ipcRenderer.invoke('app:checkForUpdates'),
|
|
82
|
+
onFocus: (cb: () => void): Unsubscribe => {
|
|
83
|
+
ipcRenderer.on('app:focus', cb)
|
|
84
|
+
return () => ipcRenderer.removeListener('app:focus', cb)
|
|
85
|
+
},
|
|
86
|
+
onBlur: (cb: () => void): Unsubscribe => {
|
|
87
|
+
ipcRenderer.on('app:blur', cb)
|
|
88
|
+
return () => ipcRenderer.removeListener('app:blur', cb)
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
window: {
|
|
92
|
+
minimize: (): void => ipcRenderer.send('window:minimize'),
|
|
93
|
+
maximize: (): void => ipcRenderer.send('window:maximize'),
|
|
94
|
+
close: (): void => ipcRenderer.send('window:close'),
|
|
95
|
+
},
|
|
96
|
+
shell: {
|
|
97
|
+
openPath: (filePath: string): void => ipcRenderer.send('shell:openPath', filePath),
|
|
98
|
+
openInFinder: (folderPath: string): void => ipcRenderer.send('shell:openInFinder', folderPath),
|
|
99
|
+
openExternal: (url: string): void => ipcRenderer.send('shell:openExternal', url),
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
contextBridge.exposeInMainWorld('api', api)
|
|
104
|
+
|
|
105
|
+
export type ApiType = typeof api
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ProcessDetector = void 0;
|
|
4
|
+
const AI_SIGNATURES = [
|
|
5
|
+
{ pattern: /claude\s+code/i, name: 'claude code', color: '#D4A27F' },
|
|
6
|
+
{ pattern: /opencode/i, name: 'opencode', color: '#7FB5D4' },
|
|
7
|
+
{ pattern: /aider/i, name: 'aider', color: '#A27FD4' },
|
|
8
|
+
{ pattern: /continue/i, name: 'continue', color: '#7FD4A2' },
|
|
9
|
+
{ pattern: /\$\s*claude\b/, name: 'claude code', color: '#D4A27F' },
|
|
10
|
+
];
|
|
11
|
+
// Strip ANSI escape sequences before testing for shell prompt
|
|
12
|
+
const ANSI_RE = /\x1b\[[0-9;]*[mGKHFABCDJsu]|\x1b\][^\x07]*\x07|\x1b[()][AB012]/g;
|
|
13
|
+
// Prompt character must be at the start of a line (with only optional surrounding whitespace).
|
|
14
|
+
// This prevents false positives from `>` in code examples, `$` in cost strings, etc.
|
|
15
|
+
const SHELL_PROMPT_PATTERN = /(?:^|\n)\s{0,6}[$%❯>]\s{0,2}$/;
|
|
16
|
+
class ProcessDetector {
|
|
17
|
+
slidingWindow = '';
|
|
18
|
+
windowSize = 2048;
|
|
19
|
+
currentAI = null;
|
|
20
|
+
hasAI = false;
|
|
21
|
+
detectedAt = 0;
|
|
22
|
+
// Grace period after detection before checking for exit. Prevents AI TUI input
|
|
23
|
+
// prompts (e.g. opencode's "> " or "❯ ") from being misread as shell prompts.
|
|
24
|
+
gracePeriodMs = 3000;
|
|
25
|
+
detect(data) {
|
|
26
|
+
this.slidingWindow = (this.slidingWindow + data).slice(-this.windowSize);
|
|
27
|
+
if (!this.hasAI) {
|
|
28
|
+
for (const sig of AI_SIGNATURES) {
|
|
29
|
+
if (sig.pattern.test(this.slidingWindow)) {
|
|
30
|
+
this.currentAI = { name: sig.name, color: sig.color };
|
|
31
|
+
this.hasAI = true;
|
|
32
|
+
this.detectedAt = Date.now();
|
|
33
|
+
return 'detected';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// Don't check for exit too soon after detection — AI TUI apps render their
|
|
39
|
+
// own input prompts ("> ", "❯ ") immediately on startup, which would otherwise
|
|
40
|
+
// trigger a false exit and cause a re-detect → token reset cycle.
|
|
41
|
+
if (Date.now() - this.detectedAt < this.gracePeriodMs)
|
|
42
|
+
return null;
|
|
43
|
+
// Check if returned to shell prompt — strip ANSI first to avoid false matches
|
|
44
|
+
const plain = this.slidingWindow.replace(ANSI_RE, '');
|
|
45
|
+
const lastLines = plain.split('\n').slice(-3).join('\n');
|
|
46
|
+
if (SHELL_PROMPT_PATTERN.test(lastLines)) {
|
|
47
|
+
this.currentAI = null;
|
|
48
|
+
this.hasAI = false;
|
|
49
|
+
return 'exited';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
getCurrentAI() {
|
|
55
|
+
return this.currentAI;
|
|
56
|
+
}
|
|
57
|
+
reset() {
|
|
58
|
+
this.slidingWindow = '';
|
|
59
|
+
this.currentAI = null;
|
|
60
|
+
this.hasAI = false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.ProcessDetector = ProcessDetector;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
interface AIProcess {
|
|
2
|
+
name: string
|
|
3
|
+
color: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const AI_SIGNATURES: Array<{ pattern: RegExp; name: string; color: string }> = [
|
|
7
|
+
{ pattern: /claude\s+code/i, name: 'claude code', color: '#D4A27F' },
|
|
8
|
+
{ pattern: /opencode/i, name: 'opencode', color: '#7FB5D4' },
|
|
9
|
+
{ pattern: /aider/i, name: 'aider', color: '#A27FD4' },
|
|
10
|
+
{ pattern: /continue/i, name: 'continue', color: '#7FD4A2' },
|
|
11
|
+
{ pattern: /\$\s*claude\b/, name: 'claude code', color: '#D4A27F' },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
// Strip ANSI escape sequences before testing for shell prompt
|
|
15
|
+
const ANSI_RE = /\x1b\[[0-9;]*[mGKHFABCDJsu]|\x1b\][^\x07]*\x07|\x1b[()][AB012]/g
|
|
16
|
+
|
|
17
|
+
// Prompt character must be at the start of a line (with only optional surrounding whitespace).
|
|
18
|
+
// This prevents false positives from `>` in code examples, `$` in cost strings, etc.
|
|
19
|
+
const SHELL_PROMPT_PATTERN = /(?:^|\n)\s{0,6}[$%❯>]\s{0,2}$/
|
|
20
|
+
|
|
21
|
+
export class ProcessDetector {
|
|
22
|
+
private slidingWindow = ''
|
|
23
|
+
private readonly windowSize = 2048
|
|
24
|
+
private currentAI: AIProcess | null = null
|
|
25
|
+
private hasAI = false
|
|
26
|
+
private detectedAt: number = 0
|
|
27
|
+
// Grace period after detection before checking for exit. Prevents AI TUI input
|
|
28
|
+
// prompts (e.g. opencode's "> " or "❯ ") from being misread as shell prompts.
|
|
29
|
+
private readonly gracePeriodMs = 3000
|
|
30
|
+
|
|
31
|
+
detect(data: string): 'detected' | 'exited' | null {
|
|
32
|
+
this.slidingWindow = (this.slidingWindow + data).slice(-this.windowSize)
|
|
33
|
+
|
|
34
|
+
if (!this.hasAI) {
|
|
35
|
+
for (const sig of AI_SIGNATURES) {
|
|
36
|
+
if (sig.pattern.test(this.slidingWindow)) {
|
|
37
|
+
this.currentAI = { name: sig.name, color: sig.color }
|
|
38
|
+
this.hasAI = true
|
|
39
|
+
this.detectedAt = Date.now()
|
|
40
|
+
return 'detected'
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
// Don't check for exit too soon after detection — AI TUI apps render their
|
|
45
|
+
// own input prompts ("> ", "❯ ") immediately on startup, which would otherwise
|
|
46
|
+
// trigger a false exit and cause a re-detect → token reset cycle.
|
|
47
|
+
if (Date.now() - this.detectedAt < this.gracePeriodMs) return null
|
|
48
|
+
// Check if returned to shell prompt — strip ANSI first to avoid false matches
|
|
49
|
+
const plain = this.slidingWindow.replace(ANSI_RE, '')
|
|
50
|
+
const lastLines = plain.split('\n').slice(-3).join('\n')
|
|
51
|
+
if (SHELL_PROMPT_PATTERN.test(lastLines)) {
|
|
52
|
+
this.currentAI = null
|
|
53
|
+
this.hasAI = false
|
|
54
|
+
return 'exited'
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getCurrentAI(): AIProcess | null {
|
|
62
|
+
return this.currentAI
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
reset(): void {
|
|
66
|
+
this.slidingWindow = ''
|
|
67
|
+
this.currentAI = null
|
|
68
|
+
this.hasAI = false
|
|
69
|
+
}
|
|
70
|
+
}
|