@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.
Files changed (34) hide show
  1. package/out/headless/index.mjs +209 -13
  2. package/out/headless/index.mjs.map +3 -3
  3. package/out/main/index.js +85 -51
  4. package/out/preload/index.js +3 -0
  5. package/out/renderer/assets/{cssMode-DQW-brNd.js → cssMode-CYoo4t9f.js} +3 -3
  6. package/out/renderer/assets/{freemarker2-DxgOckH2.js → freemarker2--UQnPZsn.js} +1 -1
  7. package/out/renderer/assets/{handlebars-BX1Wpk_3.js → handlebars-DVDrmX0C.js} +1 -1
  8. package/out/renderer/assets/{html-t-KXioI0.js → html-D1-cXoLy.js} +1 -1
  9. package/out/renderer/assets/{htmlMode-Dya7iUjr.js → htmlMode-f5nBuprq.js} +3 -3
  10. package/out/renderer/assets/{index-D6JBcQ20.css → index-65hyKM_8.css} +16 -0
  11. package/out/renderer/assets/{index-DNqZidnO.js → index-D23nru43.js} +64 -23
  12. package/out/renderer/assets/{javascript-DZzW2adn.js → javascript-CcarFzBL.js} +2 -2
  13. package/out/renderer/assets/{jsonMode-D_Wv7XH8.js → jsonMode-BvF-xK9U.js} +3 -3
  14. package/out/renderer/assets/{liquid-BJAHAm2T.js → liquid-CHLtUKl2.js} +1 -1
  15. package/out/renderer/assets/{lspLanguageFeatures-BgMd-KJk.js → lspLanguageFeatures-B9aNeatS.js} +1 -1
  16. package/out/renderer/assets/{mdx-B6Zod3ry.js → mdx-HGDrkifZ.js} +1 -1
  17. package/out/renderer/assets/{python-Cgt13-KH.js → python-B_dPzjJ6.js} +1 -1
  18. package/out/renderer/assets/{razor-BcwFJGYS.js → razor-CHheM4ot.js} +1 -1
  19. package/out/renderer/assets/{tsMode-BTjzM6fl.js → tsMode-CdC3i1gG.js} +1 -1
  20. package/out/renderer/assets/{typescript-DZYDQEUb.js → typescript-BX6guVRK.js} +1 -1
  21. package/out/renderer/assets/{xml-CloiUoIW.js → xml-CpS-pOPE.js} +1 -1
  22. package/out/renderer/assets/{yaml-CdKdpE-z.js → yaml-Du0AjOHW.js} +1 -1
  23. package/out/renderer/index.html +2 -2
  24. package/package.json +1 -1
  25. package/src/main/bridge.ts +9 -3
  26. package/src/main/headless.ts +35 -2
  27. package/src/main/index.ts +10 -39
  28. package/src/main/orchestrator.ts +29 -1
  29. package/src/main/tui.ts +20 -8
  30. package/src/main/updateCheck.ts +40 -0
  31. package/src/preload/index.ts +6 -0
  32. package/src/renderer/App.tsx +2 -0
  33. package/src/renderer/components/AgentPicker.tsx +38 -3
  34. package/src/renderer/styles.css +16 -0
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-DNqZidnO.js";
1
+ import { l as languages } from "./index-D23nru43.js";
2
2
  const conf = {
3
3
  comments: {
4
4
  blockComment: ["<!--", "-->"]
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-DNqZidnO.js";
1
+ import { l as languages } from "./index-D23nru43.js";
2
2
  const conf = {
3
3
  comments: {
4
4
  lineComment: "#"
@@ -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-DNqZidnO.js"></script>
8
- <link rel="stylesheet" crossorigin href="./assets/index-D6JBcQ20.css">
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.7",
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": {
@@ -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,
@@ -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, getDefaultAgent, isCodingAgent, type AgentConfig } from './agents'
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 picked = await tui.showAgentPicker(agents, { initialTrackTime })
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
- import https from 'https'
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() }))
@@ -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.bridge.startSession()
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, +2 for track-time separator + row
144
- const modalHeight = agents.length + 4 + 2
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 botRow = trackRow + 1
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 navigate \u00B7 Enter select \u00B7 t track \u00B7 q quit'
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' || key === ' ') {
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
+ }
@@ -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),
@@ -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={() => onSelect(a)}
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>
@@ -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;