@phenx-inc/ctlsurf 0.3.14 → 0.3.16

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 (32) hide show
  1. package/out/headless/index.mjs +57 -8
  2. package/out/headless/index.mjs.map +2 -2
  3. package/out/main/index.js +56 -7
  4. package/out/preload/index.js +6 -0
  5. package/out/renderer/assets/{cssMode-G_SDogBL.js → cssMode-D5dPwEy5.js} +3 -3
  6. package/out/renderer/assets/{freemarker2-BzEus0h2.js → freemarker2-c5jJjQ9s.js} +1 -1
  7. package/out/renderer/assets/{handlebars-Et995f6O.js → handlebars-BTbmOxx9.js} +1 -1
  8. package/out/renderer/assets/{html-D4wgKxPD.js → html-3cIIQcxO.js} +1 -1
  9. package/out/renderer/assets/{htmlMode-DSxpefzL.js → htmlMode-DYbpW1yY.js} +3 -3
  10. package/out/renderer/assets/{index-AQ346NMi.css → index-6KvOnYL1.css} +18 -0
  11. package/out/renderer/assets/{index-ByJTqkiQ.js → index-D2MUZin7.js} +36 -23
  12. package/out/renderer/assets/{javascript-CzLoo8aq.js → javascript-CDuCMm-6.js} +2 -2
  13. package/out/renderer/assets/{jsonMode-BrwPy7fY.js → jsonMode-COLqbq0s.js} +3 -3
  14. package/out/renderer/assets/{liquid-BsfPf6YG.js → liquid-BFcqZizB.js} +1 -1
  15. package/out/renderer/assets/{lspLanguageFeatures-CxLZ421s.js → lspLanguageFeatures-CbkEcL-z.js} +1 -1
  16. package/out/renderer/assets/{mdx-CPvHIsAR.js → mdx-DyK93oEE.js} +1 -1
  17. package/out/renderer/assets/{python-Dr7dCUjG.js → python-D4lCwSVr.js} +1 -1
  18. package/out/renderer/assets/{razor-a7zjD7Y3.js → razor-DdkE9XVt.js} +1 -1
  19. package/out/renderer/assets/{tsMode-B7KLV2X6.js → tsMode-BrQ4Fsc-.js} +1 -1
  20. package/out/renderer/assets/{typescript-Cjuzf37q.js → typescript-BakbYMnC.js} +1 -1
  21. package/out/renderer/assets/{xml-Yz9xINtk.js → xml-DHDW9Xhp.js} +1 -1
  22. package/out/renderer/assets/{yaml-DtKnp5J0.js → yaml-1Ayv_J3q.js} +1 -1
  23. package/out/renderer/index.html +2 -2
  24. package/package.json +1 -1
  25. package/src/main/agents.ts +36 -1
  26. package/src/main/headless.ts +5 -3
  27. package/src/main/index.ts +4 -2
  28. package/src/main/orchestrator.ts +29 -0
  29. package/src/main/workerWs.ts +8 -6
  30. package/src/preload/index.ts +7 -0
  31. package/src/renderer/App.tsx +19 -1
  32. package/src/renderer/styles.css +18 -0
package/src/main/index.ts CHANGED
@@ -21,7 +21,7 @@ function log(...args: unknown[]): void {
21
21
  try { console.log(...args) } catch { /* EPIPE safe */ }
22
22
  }
23
23
 
24
- import { AgentConfig, getDefaultAgent, getBuiltinAgents } from './agents'
24
+ import { AgentConfig, getDefaultAgent, getAvailableAgents } from './agents'
25
25
  import { getSettingsDir } from './settingsDir'
26
26
  import { Orchestrator } from './orchestrator'
27
27
 
@@ -105,6 +105,7 @@ const orchestrator = new Orchestrator(
105
105
  onWorkerStatus: (status) => mainWindow?.webContents.send('worker:status', status),
106
106
  onWorkerMessage: (message) => mainWindow?.webContents.send('worker:message', message),
107
107
  onWorkerRegistered: (data) => mainWindow?.webContents.send('worker:registered', data),
108
+ onProjectChanged: (name) => mainWindow?.webContents.send('app:projectChanged', name),
108
109
  onCwdChanged: () => {
109
110
  mainWindow?.webContents.send('app:cwdChanged')
110
111
  updateProjectBadge(orchestrator.cwd)
@@ -165,11 +166,12 @@ ipcMain.handle('pty:setActiveTab', (_event, tabId: string) => {
165
166
  orchestrator.setActiveTab(tabId)
166
167
  })
167
168
 
168
- ipcMain.handle('agents:list', () => getBuiltinAgents())
169
+ ipcMain.handle('agents:list', () => getAvailableAgents())
169
170
  ipcMain.handle('agents:default', () => getDefaultAgent())
170
171
 
171
172
  ipcMain.handle('app:homePath', () => app.getPath('home'))
172
173
  ipcMain.handle('app:cwd', () => process.env.CTLSURF_WORKER_CWD || process.cwd())
174
+ ipcMain.handle('app:projectName', () => orchestrator.projectName)
173
175
 
174
176
  ipcMain.handle('app:browseCwd', async () => {
175
177
  if (!mainWindow) return null
@@ -40,6 +40,9 @@ export interface OrchestratorEvents {
40
40
  onWorkerMessage: (message: IncomingMessage) => void
41
41
  onWorkerRegistered: (data: { worker_id: string; folder_id: string | null; status: string }) => void
42
42
  onCwdChanged: () => void
43
+ // Human-readable name of the connected ctlsurf project (folder), or null
44
+ // when no project is connected. Optional — only the desktop header uses it.
45
+ onProjectChanged?: (name: string | null) => void
43
46
  }
44
47
 
45
48
  interface TabState {
@@ -89,6 +92,7 @@ export class Orchestrator {
89
92
  }
90
93
  private noProjectPollTimer: ReturnType<typeof setInterval> | null = null
91
94
  private noProjectPollCwd: string | null = null
95
+ private currentProjectName: string | null = null
92
96
 
93
97
  constructor(settingsDir: string, events: OrchestratorEvents) {
94
98
  this.settingsDir = settingsDir
@@ -116,12 +120,14 @@ export class Orchestrator {
116
120
  log(`[worker-ws] Registered: worker_id=${data.worker_id}, folder_id=${data.folder_id}, status=${data.status}`)
117
121
  events.onWorkerRegistered(data)
118
122
  if (!data.folder_id) {
123
+ this.setProjectName(null)
119
124
  events.onWorkerStatus('no_project')
120
125
  if (this.currentCwd && data.status !== 'pending_approval') {
121
126
  this.startNoProjectPolling(this.currentCwd)
122
127
  }
123
128
  } else {
124
129
  this.stopNoProjectPolling()
130
+ this.resolveProjectName(data.folder_id)
125
131
  }
126
132
  },
127
133
  onTerminalInput: (data: string) => {
@@ -165,6 +171,29 @@ export class Orchestrator {
165
171
  return this.currentCwd
166
172
  }
167
173
 
174
+ // Name of the connected ctlsurf project (folder) for the desktop header.
175
+ get projectName(): string | null {
176
+ return this.currentProjectName
177
+ }
178
+
179
+ private setProjectName(name: string | null): void {
180
+ if (this.currentProjectName === name) return
181
+ this.currentProjectName = name
182
+ this.events.onProjectChanged?.(name)
183
+ }
184
+
185
+ // Resolve the connected folder's human-readable name. Best-effort: a failed
186
+ // lookup just leaves the project name unset rather than blocking anything.
187
+ private async resolveProjectName(folderId: string): Promise<void> {
188
+ try {
189
+ const folder = await this.ctlsurfApi.getFolder(folderId)
190
+ const name = folder?.name ?? folder?.title
191
+ this.setProjectName(typeof name === 'string' && name ? name : null)
192
+ } catch (err) {
193
+ log(`[worker-ws] Failed to resolve project name for folder ${folderId}: ${err}`)
194
+ }
195
+ }
196
+
168
197
  get agent(): AgentConfig | null {
169
198
  return this.currentAgent
170
199
  }
@@ -47,13 +47,10 @@ export class WorkerWsClient {
47
47
  private workerId: string | null = null
48
48
  private _status: WorkerWsStatus = 'disconnected'
49
49
  private shouldReconnect = false
50
- private fingerprint: string
51
50
 
52
51
  constructor(events: WorkerWsEvents, baseUrl?: string) {
53
52
  this.events = events
54
53
  this.baseUrl = baseUrl || 'wss://app.ctlsurf.com'
55
- // Generate a stable machine fingerprint
56
- this.fingerprint = this.generateFingerprint()
57
54
  }
58
55
 
59
56
  get status(): WorkerWsStatus {
@@ -72,8 +69,12 @@ export class WorkerWsClient {
72
69
  this.baseUrl = url
73
70
  }
74
71
 
75
- private generateFingerprint(): string {
76
- const data = `${os.hostname()}:${os.userInfo().username}:${os.platform()}:${os.arch()}`
72
+ // Per-directory fingerprint: each working directory is a distinct worker, so
73
+ // multiple instances on the same machine (one per project) don't collide as a
74
+ // single worker server-side. cwd is included so the same folder maps to the
75
+ // same worker across restarts.
76
+ private generateFingerprint(cwd: string): string {
77
+ const data = `${os.hostname()}:${os.userInfo().username}:${os.platform()}:${os.arch()}:${cwd}`
77
78
  return crypto.createHash('sha256').update(data).digest('hex').slice(0, 32)
78
79
  }
79
80
 
@@ -85,7 +86,8 @@ export class WorkerWsClient {
85
86
  }
86
87
 
87
88
  connect(registration: WorkerRegistration): void {
88
- this.registration = { ...registration, fingerprint: this.fingerprint }
89
+ const fingerprint = this.generateFingerprint(registration.cwd)
90
+ this.registration = { ...registration, fingerprint }
89
91
  this.shouldReconnect = true
90
92
  this.doConnect()
91
93
  }
@@ -44,6 +44,13 @@ const api = {
44
44
  ipcRenderer.invoke('app:homePath'),
45
45
  getCwd: (): Promise<string> =>
46
46
  ipcRenderer.invoke('app:cwd'),
47
+ getProjectName: (): Promise<string | null> =>
48
+ ipcRenderer.invoke('app:projectName'),
49
+ onProjectChanged: (callback: (name: string | null) => void) => {
50
+ const listener = (_event: Electron.IpcRendererEvent, name: string | null) => callback(name)
51
+ ipcRenderer.on('app:projectChanged', listener)
52
+ return () => ipcRenderer.removeListener('app:projectChanged', listener)
53
+ },
47
54
  browseCwd: (): Promise<string | null> =>
48
55
  ipcRenderer.invoke('app:browseCwd'),
49
56
  getVersion: (): Promise<string> =>
@@ -36,6 +36,8 @@ declare global {
36
36
  getDefaultAgent: () => Promise<AgentConfig>
37
37
  getHomePath: () => Promise<string>
38
38
  getCwd: () => Promise<string>
39
+ getProjectName: () => Promise<string | null>
40
+ onProjectChanged: (callback: (name: string | null) => void) => () => void
39
41
  browseCwd: () => Promise<string | null>
40
42
  getSetting: (key: string) => Promise<string | null>
41
43
  setSetting: (key: string, value: string) => Promise<{ ok: boolean }>
@@ -112,6 +114,7 @@ export default function App() {
112
114
  const [showSettings, setShowSettings] = useState(false)
113
115
  const [wsStatus, setWsStatus] = useState('disconnected')
114
116
  const [cwd, setCwd] = useState<string | null>(null)
117
+ const [projectName, setProjectName] = useState<string | null>(null)
115
118
  const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null)
116
119
 
117
120
  // Multi-tab state
@@ -140,10 +143,17 @@ export default function App() {
140
143
  const initialCwd = await window.worker.getCwd().catch(() => window.worker.getHomePath())
141
144
  setCwd(initialCwd)
142
145
  cwdRef.current = initialCwd
146
+ // Connected ctlsurf project name for the header (null until a worker registers)
147
+ window.worker.getProjectName().then(setProjectName).catch(() => { /* ignore */ })
143
148
  }
144
149
  init()
145
150
  }, [])
146
151
 
152
+ useEffect(() => {
153
+ const unsub = window.worker.onProjectChanged((name) => setProjectName(name))
154
+ return unsub
155
+ }, [])
156
+
147
157
  useEffect(() => {
148
158
  const unsub = window.worker.onWorkerStatus((status) => setWsStatus(status))
149
159
  return unsub
@@ -404,7 +414,15 @@ export default function App() {
404
414
  return (
405
415
  <div className="app">
406
416
  <div className="titlebar">
407
- <span className="titlebar-title">ctlsurf-worker</span>
417
+ <span className="titlebar-title">
418
+ ctlsurf
419
+ {projectName && (
420
+ <>
421
+ <span className="titlebar-title-sep">·</span>
422
+ <span className="titlebar-project" title={projectName}>{projectName}</span>
423
+ </>
424
+ )}
425
+ </span>
408
426
  <div className="titlebar-controls">
409
427
  <button className="titlebar-btn" onClick={() => setShowSettings(true)} title="Settings">
410
428
  Settings
@@ -36,6 +36,24 @@ html, body, #root {
36
36
  .titlebar-title {
37
37
  font-weight: 600;
38
38
  color: #c0caf5;
39
+ display: flex;
40
+ align-items: center;
41
+ gap: 6px;
42
+ min-width: 0;
43
+ }
44
+
45
+ .titlebar-title-sep {
46
+ color: #565f89;
47
+ font-weight: 400;
48
+ }
49
+
50
+ .titlebar-project {
51
+ color: #7aa2f7;
52
+ font-weight: 600;
53
+ white-space: nowrap;
54
+ overflow: hidden;
55
+ text-overflow: ellipsis;
56
+ max-width: 280px;
39
57
  }
40
58
 
41
59
  .titlebar-controls {