@phenx-inc/ctlsurf 0.3.7 → 0.3.9
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 +254 -13
- package/out/headless/index.mjs.map +3 -3
- package/out/main/index.js +130 -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 +74 -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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { conf as conf$1, language as language$1 } from "./typescript-
|
|
2
|
-
import "./index-
|
|
1
|
+
import { conf as conf$1, language as language$1 } from "./typescript-BX6guVRK.js";
|
|
2
|
+
import "./index-D23nru43.js";
|
|
3
3
|
const conf = conf$1;
|
|
4
4
|
const language = {
|
|
5
5
|
// Set defaultToken to invalid to see what you do not tokenize yet
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { c as createWebWorker, l as languages, e as editor } from "./index-
|
|
2
|
-
import { f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter, H as HoverAdapter, b as DocumentSymbolAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, S as SelectionRangeAdapter, e as DiagnosticsAdapter } from "./lspLanguageFeatures-
|
|
3
|
-
import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-
|
|
1
|
+
import { c as createWebWorker, l as languages, e as editor } from "./index-D23nru43.js";
|
|
2
|
+
import { f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter, H as HoverAdapter, b as DocumentSymbolAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, S as SelectionRangeAdapter, e as DiagnosticsAdapter } from "./lspLanguageFeatures-B9aNeatS.js";
|
|
3
|
+
import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-B9aNeatS.js";
|
|
4
4
|
const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
|
|
5
5
|
class WorkerManager {
|
|
6
6
|
constructor(defaults) {
|
package/out/renderer/assets/{lspLanguageFeatures-BgMd-KJk.js → lspLanguageFeatures-B9aNeatS.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index-
|
|
1
|
+
import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index-D23nru43.js";
|
|
2
2
|
var DocumentUri;
|
|
3
3
|
(function(DocumentUri2) {
|
|
4
4
|
function is(value) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as createWebWorker, e as editor, U as Uri, a as MarkerTag, M as MarkerSeverity, l as languages, t as typescriptDefaults, R as Range } from "./index-
|
|
1
|
+
import { c as createWebWorker, e as editor, U as Uri, a as MarkerTag, M as MarkerSeverity, l as languages, t as typescriptDefaults, R as Range } from "./index-D23nru43.js";
|
|
2
2
|
class WorkerManager {
|
|
3
3
|
constructor(_modeId, _defaults) {
|
|
4
4
|
this._modeId = _modeId;
|
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.9",
|
|
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 {
|
|
@@ -65,6 +66,7 @@ const DEFAULT_PROFILES: Record<string, Profile> = {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
const TERM_STREAM_INTERVAL_MS = 50
|
|
69
|
+
const NO_PROJECT_POLL_MS = 5_000
|
|
68
70
|
|
|
69
71
|
export class Orchestrator {
|
|
70
72
|
private settingsDir: string
|
|
@@ -84,7 +86,10 @@ export class Orchestrator {
|
|
|
84
86
|
private settings: SettingsData = {
|
|
85
87
|
activeProfile: 'production',
|
|
86
88
|
profiles: { ...DEFAULT_PROFILES },
|
|
89
|
+
logChat: false,
|
|
87
90
|
}
|
|
91
|
+
private noProjectPollTimer: ReturnType<typeof setInterval> | null = null
|
|
92
|
+
private noProjectPollCwd: string | null = null
|
|
88
93
|
|
|
89
94
|
constructor(settingsDir: string, events: OrchestratorEvents) {
|
|
90
95
|
this.settingsDir = settingsDir
|
|
@@ -113,6 +118,11 @@ export class Orchestrator {
|
|
|
113
118
|
events.onWorkerRegistered(data)
|
|
114
119
|
if (!data.folder_id) {
|
|
115
120
|
events.onWorkerStatus('no_project')
|
|
121
|
+
if (this.currentCwd && data.status !== 'pending_approval') {
|
|
122
|
+
this.startNoProjectPolling(this.currentCwd)
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
this.stopNoProjectPolling()
|
|
116
126
|
}
|
|
117
127
|
},
|
|
118
128
|
onTerminalInput: (data: string) => {
|
|
@@ -122,6 +132,24 @@ export class Orchestrator {
|
|
|
122
132
|
})
|
|
123
133
|
|
|
124
134
|
this.bridge.setWsClient(this.workerWs)
|
|
135
|
+
this.bridge.setLoggingEnabled(!!this.settings.logChat)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Chat logging ───────────────────────────────
|
|
139
|
+
|
|
140
|
+
get logChatEnabled(): boolean {
|
|
141
|
+
return !!this.settings.logChat
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
setLogChat(enabled: boolean): void {
|
|
145
|
+
this.settings.logChat = enabled
|
|
146
|
+
this.saveSettings()
|
|
147
|
+
this.bridge.setLoggingEnabled(enabled)
|
|
148
|
+
if (!enabled) {
|
|
149
|
+
this.bridge.endSession()
|
|
150
|
+
} else if (this.activeTabId) {
|
|
151
|
+
this.bridge.startSession()
|
|
152
|
+
}
|
|
125
153
|
}
|
|
126
154
|
|
|
127
155
|
// ─── Settings ───────────────────────────────────
|
|
@@ -179,6 +207,7 @@ export class Orchestrator {
|
|
|
179
207
|
dataspacePageId: raw.ctlsurfDataspacePageId || '',
|
|
180
208
|
},
|
|
181
209
|
},
|
|
210
|
+
logChat: !!raw.logChat,
|
|
182
211
|
}
|
|
183
212
|
this.saveSettings()
|
|
184
213
|
log('[settings] Migrated legacy settings to profiles')
|
|
@@ -187,16 +216,21 @@ export class Orchestrator {
|
|
|
187
216
|
if (!this.settings.profiles.production) {
|
|
188
217
|
this.settings.profiles.production = { ...DEFAULT_PROFILES.production }
|
|
189
218
|
}
|
|
219
|
+
if (this.settings.logChat === undefined) {
|
|
220
|
+
this.settings.logChat = false
|
|
221
|
+
}
|
|
190
222
|
}
|
|
191
223
|
}
|
|
192
224
|
} catch {
|
|
193
225
|
this.settings = {
|
|
194
226
|
activeProfile: 'production',
|
|
195
227
|
profiles: { ...DEFAULT_PROFILES },
|
|
228
|
+
logChat: false,
|
|
196
229
|
}
|
|
197
230
|
}
|
|
198
231
|
|
|
199
232
|
this.applyProfile(this.getActiveProfile())
|
|
233
|
+
this.bridge.setLoggingEnabled(!!this.settings.logChat)
|
|
200
234
|
}
|
|
201
235
|
|
|
202
236
|
saveSettings(): void {
|
|
@@ -354,7 +388,9 @@ export class Orchestrator {
|
|
|
354
388
|
if (t?.termStreamTimer) clearTimeout(t.termStreamTimer)
|
|
355
389
|
})
|
|
356
390
|
|
|
357
|
-
this.
|
|
391
|
+
if (this.settings.logChat) {
|
|
392
|
+
this.bridge.startSession()
|
|
393
|
+
}
|
|
358
394
|
|
|
359
395
|
const profile = this.getActiveProfile()
|
|
360
396
|
const shouldTrack = opts?.trackTime !== undefined ? opts.trackTime : (profile.trackTime !== false)
|
|
@@ -370,6 +406,7 @@ export class Orchestrator {
|
|
|
370
406
|
if (isCodingAgent(agent)) {
|
|
371
407
|
this.connectWorkerWs(agent, cwd)
|
|
372
408
|
} else {
|
|
409
|
+
this.stopNoProjectPolling()
|
|
373
410
|
this.workerWs.disconnect()
|
|
374
411
|
this.checkProjectStatus(cwd)
|
|
375
412
|
}
|
|
@@ -456,6 +493,7 @@ export class Orchestrator {
|
|
|
456
493
|
return
|
|
457
494
|
}
|
|
458
495
|
|
|
496
|
+
this.stopNoProjectPolling()
|
|
459
497
|
this.workerWs.connect({
|
|
460
498
|
machine: os.hostname(),
|
|
461
499
|
cwd,
|
|
@@ -463,6 +501,40 @@ export class Orchestrator {
|
|
|
463
501
|
})
|
|
464
502
|
}
|
|
465
503
|
|
|
504
|
+
private startNoProjectPolling(cwd: string): void {
|
|
505
|
+
if (this.noProjectPollTimer && this.noProjectPollCwd === cwd) return
|
|
506
|
+
this.stopNoProjectPolling()
|
|
507
|
+
this.noProjectPollCwd = cwd
|
|
508
|
+
log(`[worker-ws] Polling for project folder at ${cwd}`)
|
|
509
|
+
this.noProjectPollTimer = setInterval(() => { void this.checkForProjectFolder(cwd) }, NO_PROJECT_POLL_MS)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private stopNoProjectPolling(): void {
|
|
513
|
+
if (this.noProjectPollTimer) {
|
|
514
|
+
clearInterval(this.noProjectPollTimer)
|
|
515
|
+
this.noProjectPollTimer = null
|
|
516
|
+
this.noProjectPollCwd = null
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private async checkForProjectFolder(cwd: string): Promise<void> {
|
|
521
|
+
if (this.currentCwd !== cwd || !this.currentAgent) {
|
|
522
|
+
this.stopNoProjectPolling()
|
|
523
|
+
return
|
|
524
|
+
}
|
|
525
|
+
if (!this.ctlsurfApi.getApiKey()) return
|
|
526
|
+
try {
|
|
527
|
+
const folder = await this.ctlsurfApi.findFolderByPath(cwd)
|
|
528
|
+
if (folder?.id && this.currentCwd === cwd && this.currentAgent) {
|
|
529
|
+
log(`[worker-ws] Project folder appeared (${folder.id}); reconnecting`)
|
|
530
|
+
const agent = this.currentAgent
|
|
531
|
+
this.stopNoProjectPolling()
|
|
532
|
+
this.workerWs.disconnect()
|
|
533
|
+
this.connectWorkerWs(agent, cwd)
|
|
534
|
+
}
|
|
535
|
+
} catch { /* ignore — retry on next tick */ }
|
|
536
|
+
}
|
|
537
|
+
|
|
466
538
|
private async checkProjectStatus(cwd: string): Promise<void> {
|
|
467
539
|
if (!this.ctlsurfApi.getApiKey()) {
|
|
468
540
|
this.events.onWorkerStatus('no_project')
|
|
@@ -496,6 +568,7 @@ export class Orchestrator {
|
|
|
496
568
|
// ─── Shutdown ───────────────────────────────────
|
|
497
569
|
|
|
498
570
|
async shutdown(): Promise<void> {
|
|
571
|
+
this.stopNoProjectPolling()
|
|
499
572
|
this.bridge.endSession()
|
|
500
573
|
await this.timeTracker.endAll()
|
|
501
574
|
for (const [, tab] of this.tabs) {
|
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>
|