@phenx-inc/ctlsurf 0.3.7 → 0.3.8
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/out/headless/index.mjs +209 -13
- package/out/headless/index.mjs.map +3 -3
- package/out/main/index.js +85 -51
- package/out/preload/index.js +3 -0
- package/out/renderer/assets/{cssMode-DQW-brNd.js → cssMode-CYoo4t9f.js} +3 -3
- package/out/renderer/assets/{freemarker2-DxgOckH2.js → freemarker2--UQnPZsn.js} +1 -1
- package/out/renderer/assets/{handlebars-BX1Wpk_3.js → handlebars-DVDrmX0C.js} +1 -1
- package/out/renderer/assets/{html-t-KXioI0.js → html-D1-cXoLy.js} +1 -1
- package/out/renderer/assets/{htmlMode-Dya7iUjr.js → htmlMode-f5nBuprq.js} +3 -3
- package/out/renderer/assets/{index-D6JBcQ20.css → index-65hyKM_8.css} +16 -0
- package/out/renderer/assets/{index-DNqZidnO.js → index-D23nru43.js} +64 -23
- package/out/renderer/assets/{javascript-DZzW2adn.js → javascript-CcarFzBL.js} +2 -2
- package/out/renderer/assets/{jsonMode-D_Wv7XH8.js → jsonMode-BvF-xK9U.js} +3 -3
- package/out/renderer/assets/{liquid-BJAHAm2T.js → liquid-CHLtUKl2.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-BgMd-KJk.js → lspLanguageFeatures-B9aNeatS.js} +1 -1
- package/out/renderer/assets/{mdx-B6Zod3ry.js → mdx-HGDrkifZ.js} +1 -1
- package/out/renderer/assets/{python-Cgt13-KH.js → python-B_dPzjJ6.js} +1 -1
- package/out/renderer/assets/{razor-BcwFJGYS.js → razor-CHheM4ot.js} +1 -1
- package/out/renderer/assets/{tsMode-BTjzM6fl.js → tsMode-CdC3i1gG.js} +1 -1
- package/out/renderer/assets/{typescript-DZYDQEUb.js → typescript-BX6guVRK.js} +1 -1
- package/out/renderer/assets/{xml-CloiUoIW.js → xml-CpS-pOPE.js} +1 -1
- package/out/renderer/assets/{yaml-CdKdpE-z.js → yaml-Du0AjOHW.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/src/main/bridge.ts +9 -3
- package/src/main/headless.ts +35 -2
- package/src/main/index.ts +10 -39
- package/src/main/orchestrator.ts +29 -1
- package/src/main/tui.ts +20 -8
- package/src/main/updateCheck.ts +40 -0
- package/src/preload/index.ts +6 -0
- package/src/renderer/App.tsx +2 -0
- package/src/renderer/components/AgentPicker.tsx +38 -3
- package/src/renderer/styles.css +16 -0
package/out/renderer/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>ctlsurf-worker</title>
|
|
7
|
-
<script type="module" crossorigin src="./assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-D23nru43.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="./assets/index-65hyKM_8.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phenx-inc/ctlsurf",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "Agent-agnostic terminal and desktop app for ctlsurf — run Claude Code, Codex, or any coding agent with live session logging and remote control",
|
|
5
5
|
"main": "out/main/index.js",
|
|
6
6
|
"bin": {
|
package/src/main/bridge.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { SerializeAddon } from '@xterm/addon-serialize'
|
|
|
13
13
|
export class ConversationBridge {
|
|
14
14
|
private wsClient: WorkerWsClient | null = null
|
|
15
15
|
private sessionActive: boolean = false
|
|
16
|
+
private loggingEnabled: boolean = false
|
|
16
17
|
private inputBuffer: string = ''
|
|
17
18
|
private outputBuffer: string = ''
|
|
18
19
|
private outputFlushTimer: ReturnType<typeof setTimeout> | null = null
|
|
@@ -24,7 +25,12 @@ export class ConversationBridge {
|
|
|
24
25
|
this.wsClient = ws
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
setLoggingEnabled(enabled: boolean): void {
|
|
29
|
+
this.loggingEnabled = enabled
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
startSession(): void {
|
|
33
|
+
if (!this.loggingEnabled) return
|
|
28
34
|
this.clearOutputTimers()
|
|
29
35
|
this.outputBuffer = ''
|
|
30
36
|
this.inputBuffer = ''
|
|
@@ -34,7 +40,7 @@ export class ConversationBridge {
|
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
feedOutput(data: string): void {
|
|
37
|
-
if (!this.sessionActive) return
|
|
43
|
+
if (!this.sessionActive || !this.loggingEnabled) return
|
|
38
44
|
|
|
39
45
|
this.outputBuffer += data
|
|
40
46
|
this.terminalCapture.write(data)
|
|
@@ -42,7 +48,7 @@ export class ConversationBridge {
|
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
feedInput(data: string): void {
|
|
45
|
-
if (!this.sessionActive) return
|
|
51
|
+
if (!this.sessionActive || !this.loggingEnabled) return
|
|
46
52
|
this.inputBuffer += data
|
|
47
53
|
|
|
48
54
|
if (data.includes('\r') || data.includes('\n')) {
|
|
@@ -71,7 +77,7 @@ export class ConversationBridge {
|
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
private sendEntry(type: string, content: string): void {
|
|
74
|
-
if (!this.wsClient) return
|
|
80
|
+
if (!this.wsClient || !this.loggingEnabled) return
|
|
75
81
|
this.wsClient.sendChatLog({
|
|
76
82
|
ts: new Date().toISOString(),
|
|
77
83
|
type,
|
package/src/main/headless.ts
CHANGED
|
@@ -23,8 +23,19 @@ process.on('uncaughtException', (err) => {
|
|
|
23
23
|
|
|
24
24
|
import { Orchestrator } from './orchestrator'
|
|
25
25
|
import { getSettingsDir } from './settingsDir'
|
|
26
|
-
import { getBuiltinAgents,
|
|
26
|
+
import { getBuiltinAgents, isCodingAgent, type AgentConfig } from './agents'
|
|
27
27
|
import { Tui } from './tui'
|
|
28
|
+
import { fetchLatestNpmVersion, compareSemver, NPM_PACKAGE } from './updateCheck'
|
|
29
|
+
|
|
30
|
+
function getCurrentVersion(): string {
|
|
31
|
+
try {
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
33
|
+
const pkg = require('../../package.json')
|
|
34
|
+
return typeof pkg?.version === 'string' ? pkg.version : '0.0.0'
|
|
35
|
+
} catch {
|
|
36
|
+
return '0.0.0'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
28
39
|
|
|
29
40
|
// ─── CLI arg parsing ──────────────────────────────
|
|
30
41
|
|
|
@@ -63,10 +74,28 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
63
74
|
|
|
64
75
|
// ─── Main ─────────────────────────────────────────
|
|
65
76
|
|
|
77
|
+
async function checkVersionAndNotify(): Promise<void> {
|
|
78
|
+
const current = getCurrentVersion()
|
|
79
|
+
const latest = await fetchLatestNpmVersion(3000)
|
|
80
|
+
if (!latest) return
|
|
81
|
+
if (compareSemver(latest, current) <= 0) return
|
|
82
|
+
|
|
83
|
+
const Y = '\x1b[33m'
|
|
84
|
+
const G = '\x1b[32m'
|
|
85
|
+
const D = '\x1b[90m'
|
|
86
|
+
const R = '\x1b[0m'
|
|
87
|
+
process.stdout.write(
|
|
88
|
+
`\n${Y}A new version of ctlsurf is available${R} ${D}(${current} → ${latest})${R}\n` +
|
|
89
|
+
` Update: ${G}npm i -g ${NPM_PACKAGE}${R}\n\n`
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
66
93
|
async function main() {
|
|
67
94
|
const args = parseArgs(process.argv.slice(2))
|
|
68
95
|
const settingsDir = getSettingsDir(false)
|
|
69
96
|
|
|
97
|
+
await checkVersionAndNotify()
|
|
98
|
+
|
|
70
99
|
const tui = new Tui()
|
|
71
100
|
const agents = getBuiltinAgents()
|
|
72
101
|
|
|
@@ -115,9 +144,13 @@ async function main() {
|
|
|
115
144
|
}
|
|
116
145
|
} else {
|
|
117
146
|
const initialTrackTime = orchestrator.getActiveProfile().trackTime !== false
|
|
118
|
-
const
|
|
147
|
+
const initialLogChat = orchestrator.logChatEnabled
|
|
148
|
+
const picked = await tui.showAgentPicker(agents, { initialTrackTime, initialLogChat })
|
|
119
149
|
agent = agents[picked.agentIdx]
|
|
120
150
|
trackTimeOverride = picked.trackTime
|
|
151
|
+
if (picked.logChat !== orchestrator.logChatEnabled) {
|
|
152
|
+
orchestrator.setLogChat(picked.logChat)
|
|
153
|
+
}
|
|
121
154
|
}
|
|
122
155
|
|
|
123
156
|
// ─── Start TUI + agent ─────────────────────────
|
package/src/main/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { app, BrowserWindow, ipcMain, dialog, nativeImage } from 'electron'
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import fs from 'fs'
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
import { fetchLatestNpmVersion, compareSemver } from './updateCheck'
|
|
5
6
|
|
|
6
7
|
// Prevent EPIPE crashes when stdout pipe is closed
|
|
7
8
|
process.stdout?.on?.('error', () => {})
|
|
@@ -177,7 +178,6 @@ ipcMain.handle('app:browseCwd', async () => {
|
|
|
177
178
|
|
|
178
179
|
// ─── Version + npm update check ───────────────────
|
|
179
180
|
|
|
180
|
-
const NPM_PACKAGE = '@phenx-inc/ctlsurf'
|
|
181
181
|
const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000 // 6 hours
|
|
182
182
|
|
|
183
183
|
interface UpdateInfo {
|
|
@@ -194,43 +194,6 @@ let updateInfo: UpdateInfo = {
|
|
|
194
194
|
checkedAt: null,
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
function compareSemver(a: string, b: string): number {
|
|
198
|
-
const pa = a.split('.').map(n => parseInt(n, 10) || 0)
|
|
199
|
-
const pb = b.split('.').map(n => parseInt(n, 10) || 0)
|
|
200
|
-
for (let i = 0; i < 3; i++) {
|
|
201
|
-
const ai = pa[i] || 0
|
|
202
|
-
const bi = pb[i] || 0
|
|
203
|
-
if (ai !== bi) return ai - bi
|
|
204
|
-
}
|
|
205
|
-
return 0
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function fetchLatestNpmVersion(): Promise<string | null> {
|
|
209
|
-
return new Promise((resolve) => {
|
|
210
|
-
const url = `https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE)}/latest`
|
|
211
|
-
const req = https.get(url, { headers: { 'Accept': 'application/json' } }, (res) => {
|
|
212
|
-
if (res.statusCode !== 200) {
|
|
213
|
-
res.resume()
|
|
214
|
-
resolve(null)
|
|
215
|
-
return
|
|
216
|
-
}
|
|
217
|
-
let body = ''
|
|
218
|
-
res.setEncoding('utf8')
|
|
219
|
-
res.on('data', (chunk) => { body += chunk })
|
|
220
|
-
res.on('end', () => {
|
|
221
|
-
try {
|
|
222
|
-
const json = JSON.parse(body)
|
|
223
|
-
resolve(typeof json?.version === 'string' ? json.version : null)
|
|
224
|
-
} catch {
|
|
225
|
-
resolve(null)
|
|
226
|
-
}
|
|
227
|
-
})
|
|
228
|
-
})
|
|
229
|
-
req.on('error', () => resolve(null))
|
|
230
|
-
req.setTimeout(8000, () => { req.destroy(); resolve(null) })
|
|
231
|
-
})
|
|
232
|
-
}
|
|
233
|
-
|
|
234
197
|
async function checkForUpdate(): Promise<void> {
|
|
235
198
|
const latest = await fetchLatestNpmVersion()
|
|
236
199
|
updateInfo = {
|
|
@@ -381,6 +344,14 @@ ipcMain.handle('profiles:save', (_event, id: string, data: any) => {
|
|
|
381
344
|
ipcMain.handle('profiles:switch', (_event, id: string) => orchestrator.switchProfile(id))
|
|
382
345
|
ipcMain.handle('profiles:delete', (_event, id: string) => orchestrator.deleteProfile(id))
|
|
383
346
|
|
|
347
|
+
// ─── Chat logging IPC ─────────────────────────────
|
|
348
|
+
|
|
349
|
+
ipcMain.handle('logchat:get', () => ({ enabled: orchestrator.logChatEnabled }))
|
|
350
|
+
ipcMain.handle('logchat:set', (_event, enabled: boolean) => {
|
|
351
|
+
orchestrator.setLogChat(!!enabled)
|
|
352
|
+
return { enabled: orchestrator.logChatEnabled }
|
|
353
|
+
})
|
|
354
|
+
|
|
384
355
|
// ─── Tracking IPC ─────────────────────────────────
|
|
385
356
|
|
|
386
357
|
ipcMain.handle('tracking:get', () => ({ active: orchestrator.isActiveTabTracking() }))
|
package/src/main/orchestrator.ts
CHANGED
|
@@ -32,6 +32,7 @@ export interface SettingsData {
|
|
|
32
32
|
ctlsurfApiKey?: string
|
|
33
33
|
ctlsurfBaseUrl?: string
|
|
34
34
|
ctlsurfDataspacePageId?: string
|
|
35
|
+
logChat?: boolean
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export interface OrchestratorEvents {
|
|
@@ -84,6 +85,7 @@ export class Orchestrator {
|
|
|
84
85
|
private settings: SettingsData = {
|
|
85
86
|
activeProfile: 'production',
|
|
86
87
|
profiles: { ...DEFAULT_PROFILES },
|
|
88
|
+
logChat: false,
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
constructor(settingsDir: string, events: OrchestratorEvents) {
|
|
@@ -122,6 +124,24 @@ export class Orchestrator {
|
|
|
122
124
|
})
|
|
123
125
|
|
|
124
126
|
this.bridge.setWsClient(this.workerWs)
|
|
127
|
+
this.bridge.setLoggingEnabled(!!this.settings.logChat)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Chat logging ───────────────────────────────
|
|
131
|
+
|
|
132
|
+
get logChatEnabled(): boolean {
|
|
133
|
+
return !!this.settings.logChat
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
setLogChat(enabled: boolean): void {
|
|
137
|
+
this.settings.logChat = enabled
|
|
138
|
+
this.saveSettings()
|
|
139
|
+
this.bridge.setLoggingEnabled(enabled)
|
|
140
|
+
if (!enabled) {
|
|
141
|
+
this.bridge.endSession()
|
|
142
|
+
} else if (this.activeTabId) {
|
|
143
|
+
this.bridge.startSession()
|
|
144
|
+
}
|
|
125
145
|
}
|
|
126
146
|
|
|
127
147
|
// ─── Settings ───────────────────────────────────
|
|
@@ -179,6 +199,7 @@ export class Orchestrator {
|
|
|
179
199
|
dataspacePageId: raw.ctlsurfDataspacePageId || '',
|
|
180
200
|
},
|
|
181
201
|
},
|
|
202
|
+
logChat: !!raw.logChat,
|
|
182
203
|
}
|
|
183
204
|
this.saveSettings()
|
|
184
205
|
log('[settings] Migrated legacy settings to profiles')
|
|
@@ -187,16 +208,21 @@ export class Orchestrator {
|
|
|
187
208
|
if (!this.settings.profiles.production) {
|
|
188
209
|
this.settings.profiles.production = { ...DEFAULT_PROFILES.production }
|
|
189
210
|
}
|
|
211
|
+
if (this.settings.logChat === undefined) {
|
|
212
|
+
this.settings.logChat = false
|
|
213
|
+
}
|
|
190
214
|
}
|
|
191
215
|
}
|
|
192
216
|
} catch {
|
|
193
217
|
this.settings = {
|
|
194
218
|
activeProfile: 'production',
|
|
195
219
|
profiles: { ...DEFAULT_PROFILES },
|
|
220
|
+
logChat: false,
|
|
196
221
|
}
|
|
197
222
|
}
|
|
198
223
|
|
|
199
224
|
this.applyProfile(this.getActiveProfile())
|
|
225
|
+
this.bridge.setLoggingEnabled(!!this.settings.logChat)
|
|
200
226
|
}
|
|
201
227
|
|
|
202
228
|
saveSettings(): void {
|
|
@@ -354,7 +380,9 @@ export class Orchestrator {
|
|
|
354
380
|
if (t?.termStreamTimer) clearTimeout(t.termStreamTimer)
|
|
355
381
|
})
|
|
356
382
|
|
|
357
|
-
this.
|
|
383
|
+
if (this.settings.logChat) {
|
|
384
|
+
this.bridge.startSession()
|
|
385
|
+
}
|
|
358
386
|
|
|
359
387
|
const profile = this.getActiveProfile()
|
|
360
388
|
const shouldTrack = opts?.trackTime !== undefined ? opts.trackTime : (profile.trackTime !== false)
|
package/src/main/tui.ts
CHANGED
|
@@ -134,14 +134,15 @@ export class Tui {
|
|
|
134
134
|
*/
|
|
135
135
|
showAgentPicker(
|
|
136
136
|
agents: { name: string; description: string }[],
|
|
137
|
-
options: { initialTrackTime: boolean },
|
|
138
|
-
): Promise<{ agentIdx: number; trackTime: boolean }> {
|
|
137
|
+
options: { initialTrackTime: boolean; initialLogChat: boolean },
|
|
138
|
+
): Promise<{ agentIdx: number; trackTime: boolean; logChat: boolean }> {
|
|
139
139
|
return new Promise((resolve) => {
|
|
140
140
|
let selected = 0
|
|
141
141
|
let trackTime = options.initialTrackTime
|
|
142
|
+
let logChat = options.initialLogChat
|
|
142
143
|
const modalWidth = 44
|
|
143
|
-
// +4 for borders/title/sep, +
|
|
144
|
-
const modalHeight = agents.length + 4 +
|
|
144
|
+
// +4 for borders/title/sep, +3 for separator + track-time row + log-chat row
|
|
145
|
+
const modalHeight = agents.length + 4 + 3
|
|
145
146
|
const startCol = Math.max(1, Math.floor((this.cols - modalWidth) / 2))
|
|
146
147
|
const startRow = Math.max(1, Math.floor((this.rows - modalHeight) / 2))
|
|
147
148
|
|
|
@@ -197,10 +198,18 @@ export class Tui {
|
|
|
197
198
|
const trackPad = ' '.repeat(Math.max(0, modalWidth - 2 - trackContentLen))
|
|
198
199
|
this.write(`${CSI}${trackRow};${startCol}H${BG_MODAL}${FG_DIM}│${RESET}${BG_MODAL}${trackContent}${trackPad}${FG_DIM}│${RESET}`)
|
|
199
200
|
|
|
200
|
-
const
|
|
201
|
+
const logRow = trackRow + 1
|
|
202
|
+
const logCheckbox = logChat ? `${FG_GREEN}[\u2713]${RESET}${BG_MODAL}` : `${FG_DIM}[ ]${RESET}${BG_MODAL}`
|
|
203
|
+
const logLabelFg = logChat ? FG_WHITE : FG_DIM
|
|
204
|
+
const logContent = ` ${logCheckbox} ${logLabelFg}Log chat${RESET}${BG_MODAL}`
|
|
205
|
+
const logContentLen = 2 + 3 + 1 + 'Log chat'.length
|
|
206
|
+
const logPad = ' '.repeat(Math.max(0, modalWidth - 2 - logContentLen))
|
|
207
|
+
this.write(`${CSI}${logRow};${startCol}H${BG_MODAL}${FG_DIM}\u2502${RESET}${BG_MODAL}${logContent}${logPad}${FG_DIM}\u2502${RESET}`)
|
|
208
|
+
|
|
209
|
+
const botRow = logRow + 1
|
|
201
210
|
this.write(`${CSI}${botRow};${startCol}H${BG_MODAL}${FG_DIM}${botBorder}${RESET}`)
|
|
202
211
|
|
|
203
|
-
const hint = '\u2191\u2193
|
|
212
|
+
const hint = '\u2191\u2193 nav \u00B7 Enter \u00B7 t track \u00B7 l log \u00B7 q quit'
|
|
204
213
|
const hintCol = Math.max(1, Math.floor((this.cols - hint.length) / 2))
|
|
205
214
|
this.write(`${CSI}${botRow + 2};${hintCol}H${FG_DIM}${hint}${RESET}`)
|
|
206
215
|
}
|
|
@@ -221,12 +230,15 @@ export class Tui {
|
|
|
221
230
|
} else if (key === '\x1b[B' || key === 'j') {
|
|
222
231
|
selected = (selected + 1) % agents.length
|
|
223
232
|
drawModal()
|
|
224
|
-
} else if (key === 't' || key === 'T'
|
|
233
|
+
} else if (key === 't' || key === 'T') {
|
|
225
234
|
trackTime = !trackTime
|
|
226
235
|
drawModal()
|
|
236
|
+
} else if (key === 'l' || key === 'L') {
|
|
237
|
+
logChat = !logChat
|
|
238
|
+
drawModal()
|
|
227
239
|
} else if (key === '\r' || key === '\n') {
|
|
228
240
|
cleanup()
|
|
229
|
-
resolve({ agentIdx: selected, trackTime })
|
|
241
|
+
resolve({ agentIdx: selected, trackTime, logChat })
|
|
230
242
|
} else if (key === 'q' || key === '\x1b' || key === '\x03') {
|
|
231
243
|
cleanup()
|
|
232
244
|
this.write(`${CSI}?25h`)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import https from 'https'
|
|
2
|
+
|
|
3
|
+
export const NPM_PACKAGE = '@phenx-inc/ctlsurf'
|
|
4
|
+
|
|
5
|
+
export function compareSemver(a: string, b: string): number {
|
|
6
|
+
const pa = a.split('.').map(n => parseInt(n, 10) || 0)
|
|
7
|
+
const pb = b.split('.').map(n => parseInt(n, 10) || 0)
|
|
8
|
+
for (let i = 0; i < 3; i++) {
|
|
9
|
+
const ai = pa[i] || 0
|
|
10
|
+
const bi = pb[i] || 0
|
|
11
|
+
if (ai !== bi) return ai - bi
|
|
12
|
+
}
|
|
13
|
+
return 0
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function fetchLatestNpmVersion(timeoutMs = 8000): Promise<string | null> {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE)}/latest`
|
|
19
|
+
const req = https.get(url, { headers: { 'Accept': 'application/json' } }, (res) => {
|
|
20
|
+
if (res.statusCode !== 200) {
|
|
21
|
+
res.resume()
|
|
22
|
+
resolve(null)
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
let body = ''
|
|
26
|
+
res.setEncoding('utf8')
|
|
27
|
+
res.on('data', (chunk) => { body += chunk })
|
|
28
|
+
res.on('end', () => {
|
|
29
|
+
try {
|
|
30
|
+
const json = JSON.parse(body)
|
|
31
|
+
resolve(typeof json?.version === 'string' ? json.version : null)
|
|
32
|
+
} catch {
|
|
33
|
+
resolve(null)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
req.on('error', () => resolve(null))
|
|
38
|
+
req.setTimeout(timeoutMs, () => { req.destroy(); resolve(null) })
|
|
39
|
+
})
|
|
40
|
+
}
|
package/src/preload/index.ts
CHANGED
|
@@ -82,6 +82,12 @@ const api = {
|
|
|
82
82
|
setTracking: (enabled: boolean): Promise<{ active: boolean }> =>
|
|
83
83
|
ipcRenderer.invoke('tracking:set', enabled),
|
|
84
84
|
|
|
85
|
+
// Chat logging (global)
|
|
86
|
+
getLogChat: (): Promise<{ enabled: boolean }> =>
|
|
87
|
+
ipcRenderer.invoke('logchat:get'),
|
|
88
|
+
setLogChat: (enabled: boolean): Promise<{ enabled: boolean }> =>
|
|
89
|
+
ipcRenderer.invoke('logchat:set', enabled),
|
|
90
|
+
|
|
85
91
|
// Filesystem
|
|
86
92
|
readDir: (dirPath: string): Promise<Array<{ name: string; path: string; isDirectory: boolean }>> =>
|
|
87
93
|
ipcRenderer.invoke('fs:readDir', dirPath),
|
package/src/renderer/App.tsx
CHANGED
|
@@ -46,6 +46,8 @@ declare global {
|
|
|
46
46
|
deleteProfile: (profileId: string) => Promise<{ ok: boolean }>
|
|
47
47
|
getTracking: () => Promise<{ active: boolean }>
|
|
48
48
|
setTracking: (enabled: boolean) => Promise<{ active: boolean }>
|
|
49
|
+
getLogChat: () => Promise<{ enabled: boolean }>
|
|
50
|
+
setLogChat: (enabled: boolean) => Promise<{ enabled: boolean }>
|
|
49
51
|
createProject: () => Promise<{ ok: boolean; folder_id?: string; error?: string }>
|
|
50
52
|
getWebviewInfo: () => Promise<{
|
|
51
53
|
frontendUrl: string; pageUrl?: string; authenticated: boolean;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
|
|
1
3
|
interface AgentConfig {
|
|
2
4
|
id: string
|
|
3
5
|
name: string
|
|
@@ -14,9 +16,32 @@ interface AgentPickerProps {
|
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export function AgentPicker({ agents, cwd, onSelect, onChangeCwd }: AgentPickerProps) {
|
|
17
|
-
const home = typeof window !== 'undefined' ? '' : ''
|
|
18
|
-
// Shorten home dir for display
|
|
19
19
|
const displayPath = cwd.replace(/^\/Users\/[^/]+/, '~')
|
|
20
|
+
const [logChat, setLogChat] = useState(false)
|
|
21
|
+
const [loaded, setLoaded] = useState(false)
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
let cancelled = false
|
|
25
|
+
window.worker.getLogChat().then(r => {
|
|
26
|
+
if (cancelled) return
|
|
27
|
+
setLogChat(!!r?.enabled)
|
|
28
|
+
setLoaded(true)
|
|
29
|
+
}).catch(() => setLoaded(true))
|
|
30
|
+
return () => { cancelled = true }
|
|
31
|
+
}, [])
|
|
32
|
+
|
|
33
|
+
const toggleLogChat = async () => {
|
|
34
|
+
const next = !logChat
|
|
35
|
+
setLogChat(next)
|
|
36
|
+
try { await window.worker.setLogChat(next) } catch { /* ignore */ }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const handleSelect = async (agent: AgentConfig) => {
|
|
40
|
+
if (loaded) {
|
|
41
|
+
try { await window.worker.setLogChat(logChat) } catch { /* ignore */ }
|
|
42
|
+
}
|
|
43
|
+
onSelect(agent)
|
|
44
|
+
}
|
|
20
45
|
|
|
21
46
|
return (
|
|
22
47
|
<div className="agent-picker-overlay">
|
|
@@ -35,13 +60,23 @@ export function AgentPicker({ agents, cwd, onSelect, onChangeCwd }: AgentPickerP
|
|
|
35
60
|
<button
|
|
36
61
|
key={a.id}
|
|
37
62
|
className="agent-picker-item"
|
|
38
|
-
onClick={() =>
|
|
63
|
+
onClick={() => handleSelect(a)}
|
|
39
64
|
>
|
|
40
65
|
<span className="agent-picker-name">{a.name}</span>
|
|
41
66
|
<span className="agent-picker-desc">{a.description}</span>
|
|
42
67
|
</button>
|
|
43
68
|
))}
|
|
44
69
|
</div>
|
|
70
|
+
|
|
71
|
+
<label className="agent-picker-option" onClick={(e) => e.stopPropagation()}>
|
|
72
|
+
<input
|
|
73
|
+
type="checkbox"
|
|
74
|
+
checked={logChat}
|
|
75
|
+
onChange={toggleLogChat}
|
|
76
|
+
/>
|
|
77
|
+
<span>Log chat to ctlsurf</span>
|
|
78
|
+
</label>
|
|
79
|
+
|
|
45
80
|
<div className="agent-picker-hint">Select an agent to start</div>
|
|
46
81
|
</div>
|
|
47
82
|
</div>
|
package/src/renderer/styles.css
CHANGED
|
@@ -821,6 +821,22 @@ html, body, #root {
|
|
|
821
821
|
color: #414868;
|
|
822
822
|
}
|
|
823
823
|
|
|
824
|
+
.agent-picker-option {
|
|
825
|
+
margin-top: 16px;
|
|
826
|
+
display: flex;
|
|
827
|
+
align-items: center;
|
|
828
|
+
gap: 8px;
|
|
829
|
+
font-size: 12px;
|
|
830
|
+
color: #a9b1d6;
|
|
831
|
+
cursor: pointer;
|
|
832
|
+
user-select: none;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
.agent-picker-option input[type="checkbox"] {
|
|
836
|
+
cursor: pointer;
|
|
837
|
+
accent-color: #7aa2f7;
|
|
838
|
+
}
|
|
839
|
+
|
|
824
840
|
/* Settings dialog */
|
|
825
841
|
.settings-overlay {
|
|
826
842
|
position: fixed;
|