@phenx-inc/ctlsurf 0.2.0 → 0.3.1
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 +320 -44
- package/out/headless/index.mjs.map +4 -4
- package/out/main/index.js +275 -15
- package/out/preload/index.js +3 -0
- package/out/renderer/assets/{cssMode-D3kH1Kju.js → cssMode-BW-SuYuP.js} +3 -3
- package/out/renderer/assets/{freemarker2-BCHZUSLb.js → freemarker2-2YWYzawi.js} +1 -1
- package/out/renderer/assets/{handlebars-DKx-Fw-H.js → handlebars-EwtUQRsf.js} +1 -1
- package/out/renderer/assets/{html-BSCM04uL.js → html-BNZkIDb9.js} +1 -1
- package/out/renderer/assets/{htmlMode-BucU1MUc.js → htmlMode-C2dZKrOy.js} +3 -3
- package/out/renderer/assets/{index-BsdOeO0U.js → index-Bm_rbVP-.js} +114 -34
- package/out/renderer/assets/{index-BzF7I1my.css → index-CrTu3Z4M.css} +21 -0
- package/out/renderer/assets/{javascript-bPY5C4uq.js → javascript-busdVZMv.js} +2 -2
- package/out/renderer/assets/{jsonMode-BmJotb6E.js → jsonMode-BaVI6jAw.js} +3 -3
- package/out/renderer/assets/{liquid-Cja_Pzh3.js → liquid-DG08un1Q.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-hoVZfVKv.js → lspLanguageFeatures-peGVtLxi.js} +1 -1
- package/out/renderer/assets/{mdx-C0s81MOq.js → mdx-DogBhUxZ.js} +1 -1
- package/out/renderer/assets/{python-CulkBOJr.js → python-Bf-INYXh.js} +1 -1
- package/out/renderer/assets/{razor-czmzhwVZ.js → razor-DLrZ2hsF.js} +1 -1
- package/out/renderer/assets/{tsMode-B90EqYGx.js → tsMode-B4oEmliC.js} +1 -1
- package/out/renderer/assets/{typescript-Ckc6emP2.js → typescript-CjkgfhVK.js} +1 -1
- package/out/renderer/assets/{xml-CKh-JyGN.js → xml-0FAXmuVg.js} +1 -1
- package/out/renderer/assets/{yaml-B49zLim4.js → yaml-DWxnPuy8.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/src/main/ctlsurfApi.ts +26 -0
- package/src/main/headless.ts +33 -28
- package/src/main/index.ts +8 -0
- package/src/main/orchestrator.ts +63 -2
- package/src/main/timeTracker.ts +223 -0
- package/src/main/tui.ts +25 -5
- package/src/preload/index.ts +7 -1
- package/src/renderer/App.tsx +36 -0
- package/src/renderer/components/SettingsDialog.tsx +38 -1
- package/src/renderer/components/TerminalPanel.tsx +25 -13
- package/src/renderer/styles.css +21 -0
package/out/main/index.js
CHANGED
|
@@ -176,6 +176,25 @@ class CtlsurfApi {
|
|
|
176
176
|
const folder = await this.request("GET", `/folders/${folderId}`);
|
|
177
177
|
return folder?.pages || [];
|
|
178
178
|
}
|
|
179
|
+
async getFolder(folderId) {
|
|
180
|
+
return this.request("GET", `/folders/${folderId}`);
|
|
181
|
+
}
|
|
182
|
+
// ─── Datastore ───────────────────────────────────────
|
|
183
|
+
async getPageBlockSummaries(pageId) {
|
|
184
|
+
return this.request("GET", `/blocks/page/${pageId}/summary`);
|
|
185
|
+
}
|
|
186
|
+
async addRow(blockId, data) {
|
|
187
|
+
return this.request("POST", `/datastore/${blockId}/rows`, { data });
|
|
188
|
+
}
|
|
189
|
+
async updateRow(blockId, rowId, data) {
|
|
190
|
+
return this.request("PUT", `/datastore/${blockId}/rows/${rowId}`, { data });
|
|
191
|
+
}
|
|
192
|
+
async getDatastoreSchema(blockId) {
|
|
193
|
+
return this.request("GET", `/datastore/${blockId}/schema`);
|
|
194
|
+
}
|
|
195
|
+
async updateDatastoreSchema(blockId, columns) {
|
|
196
|
+
return this.request("PUT", `/datastore/${blockId}/schema`, { columns });
|
|
197
|
+
}
|
|
179
198
|
async findFolderByGitRemote(gitRemote) {
|
|
180
199
|
const folders = await this.request("GET", "/folders");
|
|
181
200
|
return folders?.find((f) => f.git_remote === gitRemote || f.root_path === gitRemote) || null;
|
|
@@ -4185,7 +4204,7 @@ function requireWebsocketServer() {
|
|
|
4185
4204
|
}
|
|
4186
4205
|
requireWebsocketServer();
|
|
4187
4206
|
const WS = typeof WebSocket !== "undefined" ? WebSocket : WebSocket$1;
|
|
4188
|
-
function log$
|
|
4207
|
+
function log$3(...args) {
|
|
4189
4208
|
try {
|
|
4190
4209
|
console.log(...args);
|
|
4191
4210
|
} catch {
|
|
@@ -4281,7 +4300,7 @@ class WorkerWsClient {
|
|
|
4281
4300
|
}
|
|
4282
4301
|
doConnect() {
|
|
4283
4302
|
if (!this.apiKey || !this.registration) {
|
|
4284
|
-
log$
|
|
4303
|
+
log$3("[worker-ws] No API key or registration, skipping connect");
|
|
4285
4304
|
return;
|
|
4286
4305
|
}
|
|
4287
4306
|
this.clearTimers();
|
|
@@ -4304,22 +4323,22 @@ class WorkerWsClient {
|
|
|
4304
4323
|
doConnectNow() {
|
|
4305
4324
|
if (!this.apiKey || !this.registration) return;
|
|
4306
4325
|
if (!this.shouldReconnect) {
|
|
4307
|
-
log$
|
|
4326
|
+
log$3("[worker-ws] shouldReconnect is false, aborting connect");
|
|
4308
4327
|
return;
|
|
4309
4328
|
}
|
|
4310
4329
|
this.setStatus("connecting");
|
|
4311
4330
|
const wsBase = this.baseUrl.replace(/^http/, "ws");
|
|
4312
4331
|
const url = `${wsBase}/api/ws/worker?token=${encodeURIComponent(this.apiKey)}`;
|
|
4313
|
-
log$
|
|
4332
|
+
log$3(`[worker-ws] Connecting to ${url.replace(/token=.*/, "token=***")}...`);
|
|
4314
4333
|
try {
|
|
4315
4334
|
this.ws = new WS(url);
|
|
4316
4335
|
} catch (err) {
|
|
4317
|
-
log$
|
|
4336
|
+
log$3("[worker-ws] Failed to create WebSocket:", err);
|
|
4318
4337
|
this.scheduleReconnect();
|
|
4319
4338
|
return;
|
|
4320
4339
|
}
|
|
4321
4340
|
this.ws.onopen = () => {
|
|
4322
|
-
log$
|
|
4341
|
+
log$3("[worker-ws] Connected, sending register");
|
|
4323
4342
|
this.reconnectDelay = RECONNECT_DELAY_MS;
|
|
4324
4343
|
this.send({
|
|
4325
4344
|
type: "register",
|
|
@@ -4332,11 +4351,11 @@ class WorkerWsClient {
|
|
|
4332
4351
|
const data = JSON.parse(String(event.data));
|
|
4333
4352
|
this.handleMessage(data);
|
|
4334
4353
|
} catch (err) {
|
|
4335
|
-
log$
|
|
4354
|
+
log$3("[worker-ws] Failed to parse message:", err);
|
|
4336
4355
|
}
|
|
4337
4356
|
};
|
|
4338
4357
|
this.ws.onclose = (event) => {
|
|
4339
|
-
log$
|
|
4358
|
+
log$3(`[worker-ws] Disconnected: ${event.code} ${event.reason}`);
|
|
4340
4359
|
this.ws = null;
|
|
4341
4360
|
this.clearHeartbeat();
|
|
4342
4361
|
this.setStatus("disconnected");
|
|
@@ -4345,7 +4364,7 @@ class WorkerWsClient {
|
|
|
4345
4364
|
}
|
|
4346
4365
|
};
|
|
4347
4366
|
this.ws.onerror = () => {
|
|
4348
|
-
log$
|
|
4367
|
+
log$3("[worker-ws] WebSocket error");
|
|
4349
4368
|
};
|
|
4350
4369
|
}
|
|
4351
4370
|
handleMessage(data) {
|
|
@@ -4373,7 +4392,7 @@ class WorkerWsClient {
|
|
|
4373
4392
|
break;
|
|
4374
4393
|
}
|
|
4375
4394
|
case "approved": {
|
|
4376
|
-
log$
|
|
4395
|
+
log$3("[worker-ws] Worker approved!");
|
|
4377
4396
|
this.setStatus("connected");
|
|
4378
4397
|
break;
|
|
4379
4398
|
}
|
|
@@ -4431,18 +4450,211 @@ class WorkerWsClient {
|
|
|
4431
4450
|
}
|
|
4432
4451
|
}
|
|
4433
4452
|
}
|
|
4453
|
+
const DATASTORE_TITLE = "Time Tracking";
|
|
4454
|
+
const AGENT_DATASTORE_PAGE_TITLE = "Agent Datastore";
|
|
4455
|
+
const FIRST_CHECKPOINT_DELAY_MS = 30 * 1e3;
|
|
4456
|
+
const CHECKPOINT_INTERVAL_MS = 5 * 60 * 1e3;
|
|
4457
|
+
const COLUMNS = [
|
|
4458
|
+
{ name: "Started", type: "date" },
|
|
4459
|
+
{ name: "Active Time", type: "number" },
|
|
4460
|
+
{ name: "Agent", type: "text" },
|
|
4461
|
+
{ name: "Worker", type: "text" },
|
|
4462
|
+
{ name: "Session", type: "text" },
|
|
4463
|
+
{ name: "Notes", type: "text" }
|
|
4464
|
+
];
|
|
4465
|
+
function log$2(...args) {
|
|
4466
|
+
try {
|
|
4467
|
+
console.log("[time-tracker]", ...args);
|
|
4468
|
+
} catch {
|
|
4469
|
+
}
|
|
4470
|
+
}
|
|
4471
|
+
function findPageByTitle(pages, title) {
|
|
4472
|
+
for (const p of pages) {
|
|
4473
|
+
if (p?.title === title) return p;
|
|
4474
|
+
if (p?.children?.length) {
|
|
4475
|
+
const c = findPageByTitle(p.children, title);
|
|
4476
|
+
if (c) return c;
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
4479
|
+
return null;
|
|
4480
|
+
}
|
|
4481
|
+
class TimeTracker {
|
|
4482
|
+
api;
|
|
4483
|
+
sessions = /* @__PURE__ */ new Map();
|
|
4484
|
+
blockCache = /* @__PURE__ */ new Map();
|
|
4485
|
+
constructor(api) {
|
|
4486
|
+
this.api = api;
|
|
4487
|
+
}
|
|
4488
|
+
async startSession(tabId, cwd, agentName, idleTimeoutMin) {
|
|
4489
|
+
if (this.sessions.has(tabId)) {
|
|
4490
|
+
await this.endSession(tabId);
|
|
4491
|
+
}
|
|
4492
|
+
try {
|
|
4493
|
+
const blockId = await this.ensureDatastore(cwd);
|
|
4494
|
+
if (!blockId) {
|
|
4495
|
+
log$2(`No "${AGENT_DATASTORE_PAGE_TITLE}" page found for ${cwd} — tracking disabled for this session`);
|
|
4496
|
+
return;
|
|
4497
|
+
}
|
|
4498
|
+
const startedAt = Date.now();
|
|
4499
|
+
const startedIso = new Date(startedAt).toISOString();
|
|
4500
|
+
const sessionUuid = require$$1.randomUUID();
|
|
4501
|
+
const row = await this.api.addRow(blockId, {
|
|
4502
|
+
Started: startedIso,
|
|
4503
|
+
"Active Time": 0,
|
|
4504
|
+
Agent: agentName,
|
|
4505
|
+
Worker: os.hostname(),
|
|
4506
|
+
Session: sessionUuid,
|
|
4507
|
+
Notes: ""
|
|
4508
|
+
});
|
|
4509
|
+
const rowId = row?.id;
|
|
4510
|
+
if (!rowId) {
|
|
4511
|
+
log$2("addRow returned no id; aborting tracking", row);
|
|
4512
|
+
return;
|
|
4513
|
+
}
|
|
4514
|
+
const state = {
|
|
4515
|
+
blockId,
|
|
4516
|
+
rowId,
|
|
4517
|
+
cwd,
|
|
4518
|
+
startedAt,
|
|
4519
|
+
lastActivity: startedAt,
|
|
4520
|
+
activeMs: 0,
|
|
4521
|
+
idleTimeoutMs: Math.max(1, idleTimeoutMin) * 60 * 1e3,
|
|
4522
|
+
firstCheckpointTimer: null,
|
|
4523
|
+
checkpointTimer: null,
|
|
4524
|
+
ended: false
|
|
4525
|
+
};
|
|
4526
|
+
state.firstCheckpointTimer = setTimeout(() => {
|
|
4527
|
+
void this.checkpoint(tabId);
|
|
4528
|
+
const live = this.sessions.get(tabId);
|
|
4529
|
+
if (live && !live.ended) {
|
|
4530
|
+
live.checkpointTimer = setInterval(() => {
|
|
4531
|
+
void this.checkpoint(tabId);
|
|
4532
|
+
}, CHECKPOINT_INTERVAL_MS);
|
|
4533
|
+
}
|
|
4534
|
+
}, FIRST_CHECKPOINT_DELAY_MS);
|
|
4535
|
+
this.sessions.set(tabId, state);
|
|
4536
|
+
log$2(`Started tracking tab=${tabId} agent="${agentName}" cwd=${cwd}`);
|
|
4537
|
+
} catch (err) {
|
|
4538
|
+
log$2(`startSession failed: ${err?.message || err}`);
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
isTracking(tabId) {
|
|
4542
|
+
const s = this.sessions.get(tabId);
|
|
4543
|
+
return !!s && !s.ended;
|
|
4544
|
+
}
|
|
4545
|
+
recordActivity(tabId) {
|
|
4546
|
+
const s = this.sessions.get(tabId);
|
|
4547
|
+
if (!s || s.ended) return;
|
|
4548
|
+
const now = Date.now();
|
|
4549
|
+
const delta = now - s.lastActivity;
|
|
4550
|
+
if (delta < s.idleTimeoutMs) {
|
|
4551
|
+
s.activeMs += delta;
|
|
4552
|
+
}
|
|
4553
|
+
s.lastActivity = now;
|
|
4554
|
+
}
|
|
4555
|
+
async endSession(tabId) {
|
|
4556
|
+
const s = this.sessions.get(tabId);
|
|
4557
|
+
if (!s || s.ended) return;
|
|
4558
|
+
s.ended = true;
|
|
4559
|
+
if (s.firstCheckpointTimer) clearTimeout(s.firstCheckpointTimer);
|
|
4560
|
+
if (s.checkpointTimer) clearInterval(s.checkpointTimer);
|
|
4561
|
+
try {
|
|
4562
|
+
await this.writeRow(s, Date.now());
|
|
4563
|
+
} catch (err) {
|
|
4564
|
+
log$2(`endSession write failed: ${err?.message || err}`);
|
|
4565
|
+
}
|
|
4566
|
+
this.sessions.delete(tabId);
|
|
4567
|
+
}
|
|
4568
|
+
async endAll() {
|
|
4569
|
+
const ids = [...this.sessions.keys()];
|
|
4570
|
+
await Promise.all(ids.map((id) => this.endSession(id)));
|
|
4571
|
+
}
|
|
4572
|
+
async checkpoint(tabId) {
|
|
4573
|
+
const s = this.sessions.get(tabId);
|
|
4574
|
+
if (!s || s.ended) return;
|
|
4575
|
+
try {
|
|
4576
|
+
await this.writeRow(s, Date.now());
|
|
4577
|
+
} catch (err) {
|
|
4578
|
+
log$2(`checkpoint failed: ${err?.message || err}`);
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4581
|
+
async writeRow(s, _endTimeMs) {
|
|
4582
|
+
const activeMin = Math.round(s.activeMs / 6e4);
|
|
4583
|
+
await this.api.updateRow(s.blockId, s.rowId, {
|
|
4584
|
+
"Active Time": activeMin
|
|
4585
|
+
});
|
|
4586
|
+
}
|
|
4587
|
+
async ensureDatastore(cwd) {
|
|
4588
|
+
const cached = this.blockCache.get(cwd);
|
|
4589
|
+
if (cached) return cached;
|
|
4590
|
+
let folder = null;
|
|
4591
|
+
try {
|
|
4592
|
+
folder = await this.api.findFolderByPath(cwd);
|
|
4593
|
+
} catch {
|
|
4594
|
+
return null;
|
|
4595
|
+
}
|
|
4596
|
+
if (!folder?.id) return null;
|
|
4597
|
+
const folderDetail = await this.api.getFolder(folder.id);
|
|
4598
|
+
const agentPage = findPageByTitle(folderDetail?.pages || [], AGENT_DATASTORE_PAGE_TITLE);
|
|
4599
|
+
if (!agentPage?.id) return null;
|
|
4600
|
+
const summaries = await this.api.getPageBlockSummaries(agentPage.id);
|
|
4601
|
+
const existing = (summaries || []).find((b) => b?.type === "datastore" && b?.title === DATASTORE_TITLE);
|
|
4602
|
+
if (existing?.id) {
|
|
4603
|
+
await this.ensureColumns(existing.id);
|
|
4604
|
+
this.blockCache.set(cwd, existing.id);
|
|
4605
|
+
return existing.id;
|
|
4606
|
+
}
|
|
4607
|
+
const columns = COLUMNS.map((c, i) => ({ id: `col_${i}`, name: c.name, type: c.type }));
|
|
4608
|
+
const created = await this.api.createBlock(agentPage.id, {
|
|
4609
|
+
type: "datastore",
|
|
4610
|
+
title: DATASTORE_TITLE,
|
|
4611
|
+
props: { columns }
|
|
4612
|
+
});
|
|
4613
|
+
if (created?.id) {
|
|
4614
|
+
log$2(`Created "${DATASTORE_TITLE}" datastore on Agent Datastore page for ${cwd}`);
|
|
4615
|
+
this.blockCache.set(cwd, created.id);
|
|
4616
|
+
return created.id;
|
|
4617
|
+
}
|
|
4618
|
+
return null;
|
|
4619
|
+
}
|
|
4620
|
+
async ensureColumns(blockId) {
|
|
4621
|
+
try {
|
|
4622
|
+
const schema = await this.api.getDatastoreSchema(blockId);
|
|
4623
|
+
const existingCols = schema.columns || [];
|
|
4624
|
+
const existingNames = new Set(existingCols.map((c) => c.name));
|
|
4625
|
+
const missing = COLUMNS.filter((c) => !existingNames.has(c.name));
|
|
4626
|
+
if (missing.length === 0) return;
|
|
4627
|
+
const usedIds = new Set(existingCols.map((c) => c.id));
|
|
4628
|
+
let nextIdx = existingCols.length;
|
|
4629
|
+
const appended = missing.map((c) => {
|
|
4630
|
+
let id = `col_${nextIdx++}`;
|
|
4631
|
+
while (usedIds.has(id)) id = `col_${nextIdx++}`;
|
|
4632
|
+
usedIds.add(id);
|
|
4633
|
+
return { id, name: c.name, type: c.type };
|
|
4634
|
+
});
|
|
4635
|
+
const merged = [...existingCols, ...appended];
|
|
4636
|
+
await this.api.updateDatastoreSchema(blockId, merged);
|
|
4637
|
+
log$2(`Added ${missing.length} missing column(s) to existing Time Tracking datastore: ${missing.map((c) => c.name).join(", ")}`);
|
|
4638
|
+
} catch (err) {
|
|
4639
|
+
log$2(`ensureColumns failed: ${err?.message || err}`);
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4434
4643
|
function log$1(...args) {
|
|
4435
4644
|
try {
|
|
4436
4645
|
console.log(...args);
|
|
4437
4646
|
} catch {
|
|
4438
4647
|
}
|
|
4439
4648
|
}
|
|
4649
|
+
const DEFAULT_IDLE_TIMEOUT_MIN = 15;
|
|
4440
4650
|
const DEFAULT_PROFILES = {
|
|
4441
4651
|
production: {
|
|
4442
4652
|
name: "Production",
|
|
4443
4653
|
apiKey: "",
|
|
4444
4654
|
baseUrl: "https://app.ctlsurf.com",
|
|
4445
|
-
dataspacePageId: ""
|
|
4655
|
+
dataspacePageId: "",
|
|
4656
|
+
trackTime: true,
|
|
4657
|
+
idleTimeoutMin: 15
|
|
4446
4658
|
}
|
|
4447
4659
|
};
|
|
4448
4660
|
const TERM_STREAM_INTERVAL_MS = 50;
|
|
@@ -4453,6 +4665,7 @@ class Orchestrator {
|
|
|
4453
4665
|
ctlsurfApi = new CtlsurfApi();
|
|
4454
4666
|
bridge = new ConversationBridge();
|
|
4455
4667
|
workerWs;
|
|
4668
|
+
timeTracker = new TimeTracker(this.ctlsurfApi);
|
|
4456
4669
|
// State
|
|
4457
4670
|
tabs = /* @__PURE__ */ new Map();
|
|
4458
4671
|
activeTabId = null;
|
|
@@ -4587,7 +4800,9 @@ class Orchestrator {
|
|
|
4587
4800
|
name: p.name,
|
|
4588
4801
|
baseUrl: p.baseUrl,
|
|
4589
4802
|
hasApiKey: !!p.apiKey,
|
|
4590
|
-
dataspacePageId: p.dataspacePageId || null
|
|
4803
|
+
dataspacePageId: p.dataspacePageId || null,
|
|
4804
|
+
trackTime: p.trackTime !== false,
|
|
4805
|
+
idleTimeoutMin: p.idleTimeoutMin ?? DEFAULT_IDLE_TIMEOUT_MIN
|
|
4591
4806
|
}))
|
|
4592
4807
|
};
|
|
4593
4808
|
}
|
|
@@ -4599,7 +4814,9 @@ class Orchestrator {
|
|
|
4599
4814
|
name: p.name,
|
|
4600
4815
|
baseUrl: p.baseUrl,
|
|
4601
4816
|
hasApiKey: !!p.apiKey,
|
|
4602
|
-
dataspacePageId: p.dataspacePageId || ""
|
|
4817
|
+
dataspacePageId: p.dataspacePageId || "",
|
|
4818
|
+
trackTime: p.trackTime !== false,
|
|
4819
|
+
idleTimeoutMin: p.idleTimeoutMin ?? DEFAULT_IDLE_TIMEOUT_MIN
|
|
4603
4820
|
};
|
|
4604
4821
|
}
|
|
4605
4822
|
saveProfile(profileId, data) {
|
|
@@ -4608,7 +4825,9 @@ class Orchestrator {
|
|
|
4608
4825
|
name: data.name,
|
|
4609
4826
|
apiKey: data.apiKey !== void 0 ? data.apiKey : existing?.apiKey || "",
|
|
4610
4827
|
baseUrl: data.baseUrl || "https://app.ctlsurf.com",
|
|
4611
|
-
dataspacePageId: data.dataspacePageId || ""
|
|
4828
|
+
dataspacePageId: data.dataspacePageId || "",
|
|
4829
|
+
trackTime: data.trackTime !== void 0 ? data.trackTime : existing?.trackTime !== false,
|
|
4830
|
+
idleTimeoutMin: data.idleTimeoutMin !== void 0 ? data.idleTimeoutMin : existing?.idleTimeoutMin ?? DEFAULT_IDLE_TIMEOUT_MIN
|
|
4612
4831
|
};
|
|
4613
4832
|
this.saveSettings();
|
|
4614
4833
|
if (profileId === this.settings.activeProfile) {
|
|
@@ -4646,7 +4865,7 @@ class Orchestrator {
|
|
|
4646
4865
|
return { ok: true };
|
|
4647
4866
|
}
|
|
4648
4867
|
// ─── PTY & Agent (multi-tab) ─────────────────────
|
|
4649
|
-
async spawnAgent(tabId, agent, cwd) {
|
|
4868
|
+
async spawnAgent(tabId, agent, cwd, opts) {
|
|
4650
4869
|
const existing = this.tabs.get(tabId);
|
|
4651
4870
|
if (existing) {
|
|
4652
4871
|
if (existing.termStreamTimer) clearTimeout(existing.termStreamTimer);
|
|
@@ -4665,6 +4884,7 @@ class Orchestrator {
|
|
|
4665
4884
|
this.tabs.set(tabId, tab);
|
|
4666
4885
|
ptyManager.onData((data) => {
|
|
4667
4886
|
this.events.onPtyData(tabId, data);
|
|
4887
|
+
this.timeTracker.recordActivity(tabId);
|
|
4668
4888
|
if (tabId === this.activeTabId) {
|
|
4669
4889
|
this.bridge.feedOutput(data);
|
|
4670
4890
|
this.streamTerminalData(tabId, data);
|
|
@@ -4672,6 +4892,7 @@ class Orchestrator {
|
|
|
4672
4892
|
});
|
|
4673
4893
|
ptyManager.onExit(async (exitCode) => {
|
|
4674
4894
|
this.events.onPtyExit(tabId, exitCode);
|
|
4895
|
+
await this.timeTracker.endSession(tabId);
|
|
4675
4896
|
if (tabId === this.activeTabId) {
|
|
4676
4897
|
this.bridge.endSession();
|
|
4677
4898
|
if (this.currentAgent && isCodingAgent(this.currentAgent)) {
|
|
@@ -4682,6 +4903,16 @@ class Orchestrator {
|
|
|
4682
4903
|
if (t?.termStreamTimer) clearTimeout(t.termStreamTimer);
|
|
4683
4904
|
});
|
|
4684
4905
|
this.bridge.startSession();
|
|
4906
|
+
const profile = this.getActiveProfile();
|
|
4907
|
+
const shouldTrack = opts?.trackTime !== void 0 ? opts.trackTime : profile.trackTime !== false;
|
|
4908
|
+
if (shouldTrack) {
|
|
4909
|
+
void this.timeTracker.startSession(
|
|
4910
|
+
tabId,
|
|
4911
|
+
cwd,
|
|
4912
|
+
agent.name,
|
|
4913
|
+
profile.idleTimeoutMin ?? DEFAULT_IDLE_TIMEOUT_MIN
|
|
4914
|
+
);
|
|
4915
|
+
}
|
|
4685
4916
|
if (isCodingAgent(agent)) {
|
|
4686
4917
|
this.connectWorkerWs(agent, cwd);
|
|
4687
4918
|
} else {
|
|
@@ -4706,6 +4937,7 @@ class Orchestrator {
|
|
|
4706
4937
|
const tab = this.tabs.get(tabId);
|
|
4707
4938
|
if (!tab) return;
|
|
4708
4939
|
if (tab.termStreamTimer) clearTimeout(tab.termStreamTimer);
|
|
4940
|
+
await this.timeTracker.endSession(tabId);
|
|
4709
4941
|
tab.ptyManager.kill();
|
|
4710
4942
|
this.tabs.delete(tabId);
|
|
4711
4943
|
if (tabId === this.activeTabId) {
|
|
@@ -4728,6 +4960,28 @@ class Orchestrator {
|
|
|
4728
4960
|
getTabIds() {
|
|
4729
4961
|
return [...this.tabs.keys()];
|
|
4730
4962
|
}
|
|
4963
|
+
// ─── Tracking control (active tab) ──────────────
|
|
4964
|
+
isActiveTabTracking() {
|
|
4965
|
+
if (!this.activeTabId) return false;
|
|
4966
|
+
return this.timeTracker.isTracking(this.activeTabId);
|
|
4967
|
+
}
|
|
4968
|
+
async setActiveTabTracking(enabled) {
|
|
4969
|
+
if (!this.activeTabId) return;
|
|
4970
|
+
const tab = this.tabs.get(this.activeTabId);
|
|
4971
|
+
if (!tab) return;
|
|
4972
|
+
if (enabled) {
|
|
4973
|
+
if (this.timeTracker.isTracking(this.activeTabId)) return;
|
|
4974
|
+
const profile = this.getActiveProfile();
|
|
4975
|
+
await this.timeTracker.startSession(
|
|
4976
|
+
this.activeTabId,
|
|
4977
|
+
tab.cwd,
|
|
4978
|
+
tab.agent.name,
|
|
4979
|
+
profile.idleTimeoutMin ?? DEFAULT_IDLE_TIMEOUT_MIN
|
|
4980
|
+
);
|
|
4981
|
+
} else {
|
|
4982
|
+
await this.timeTracker.endSession(this.activeTabId);
|
|
4983
|
+
}
|
|
4984
|
+
}
|
|
4731
4985
|
// ─── Worker WebSocket ───────────────────────────
|
|
4732
4986
|
connectWorkerWs(agent, cwd) {
|
|
4733
4987
|
const profile = this.getActiveProfile();
|
|
@@ -4773,6 +5027,7 @@ class Orchestrator {
|
|
|
4773
5027
|
// ─── Shutdown ───────────────────────────────────
|
|
4774
5028
|
async shutdown() {
|
|
4775
5029
|
this.bridge.endSession();
|
|
5030
|
+
await this.timeTracker.endAll();
|
|
4776
5031
|
for (const [, tab] of this.tabs) {
|
|
4777
5032
|
if (tab.termStreamTimer) clearTimeout(tab.termStreamTimer);
|
|
4778
5033
|
tab.ptyManager.kill();
|
|
@@ -5024,6 +5279,11 @@ electron.ipcMain.handle("profiles:save", (_event, id, data) => {
|
|
|
5024
5279
|
});
|
|
5025
5280
|
electron.ipcMain.handle("profiles:switch", (_event, id) => orchestrator.switchProfile(id));
|
|
5026
5281
|
electron.ipcMain.handle("profiles:delete", (_event, id) => orchestrator.deleteProfile(id));
|
|
5282
|
+
electron.ipcMain.handle("tracking:get", () => ({ active: orchestrator.isActiveTabTracking() }));
|
|
5283
|
+
electron.ipcMain.handle("tracking:set", async (_event, enabled) => {
|
|
5284
|
+
await orchestrator.setActiveTabTracking(enabled);
|
|
5285
|
+
return { active: orchestrator.isActiveTabTracking() };
|
|
5286
|
+
});
|
|
5027
5287
|
electron.ipcMain.handle("settings:get", (_event, key) => {
|
|
5028
5288
|
const profile = orchestrator.getActiveProfile();
|
|
5029
5289
|
if (key === "ctlsurfApiKey") return profile.apiKey ? "***configured***" : null;
|
package/out/preload/index.js
CHANGED
|
@@ -35,6 +35,9 @@ const api = {
|
|
|
35
35
|
saveProfile: (profileId, data) => electron.ipcRenderer.invoke("profiles:save", profileId, data),
|
|
36
36
|
switchProfile: (profileId) => electron.ipcRenderer.invoke("profiles:switch", profileId),
|
|
37
37
|
deleteProfile: (profileId) => electron.ipcRenderer.invoke("profiles:delete", profileId),
|
|
38
|
+
// Tracking (active tab)
|
|
39
|
+
getTracking: () => electron.ipcRenderer.invoke("tracking:get"),
|
|
40
|
+
setTracking: (enabled) => electron.ipcRenderer.invoke("tracking:set", enabled),
|
|
38
41
|
// Filesystem
|
|
39
42
|
readDir: (dirPath) => electron.ipcRenderer.invoke("fs:readDir", dirPath),
|
|
40
43
|
readFile: (filePath) => electron.ipcRenderer.invoke("fs:readFile", filePath),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { c as createWebWorker, l as languages } from "./index-
|
|
2
|
-
import { C as CompletionAdapter, H as HoverAdapter, D as DocumentHighlightAdapter, a as DefinitionAdapter, R as ReferenceAdapter, b as DocumentSymbolAdapter, c as RenameAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, e as DiagnosticsAdapter, S as SelectionRangeAdapter, f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider } from "./lspLanguageFeatures-
|
|
3
|
-
import { h, i, j, t, k } from "./lspLanguageFeatures-
|
|
1
|
+
import { c as createWebWorker, l as languages } from "./index-Bm_rbVP-.js";
|
|
2
|
+
import { C as CompletionAdapter, H as HoverAdapter, D as DocumentHighlightAdapter, a as DefinitionAdapter, R as ReferenceAdapter, b as DocumentSymbolAdapter, c as RenameAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, e as DiagnosticsAdapter, S as SelectionRangeAdapter, f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider } from "./lspLanguageFeatures-peGVtLxi.js";
|
|
3
|
+
import { h, i, j, t, k } from "./lspLanguageFeatures-peGVtLxi.js";
|
|
4
4
|
const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
|
|
5
5
|
class WorkerManager {
|
|
6
6
|
constructor(defaults) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { c as createWebWorker, l as languages } from "./index-
|
|
2
|
-
import { H as HoverAdapter, D as DocumentHighlightAdapter, h as DocumentLinkAdapter, F as FoldingRangeAdapter, b as DocumentSymbolAdapter, S as SelectionRangeAdapter, c as RenameAdapter, f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter } from "./lspLanguageFeatures-
|
|
3
|
-
import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-
|
|
1
|
+
import { c as createWebWorker, l as languages } from "./index-Bm_rbVP-.js";
|
|
2
|
+
import { H as HoverAdapter, D as DocumentHighlightAdapter, h as DocumentLinkAdapter, F as FoldingRangeAdapter, b as DocumentSymbolAdapter, S as SelectionRangeAdapter, c as RenameAdapter, f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter } from "./lspLanguageFeatures-peGVtLxi.js";
|
|
3
|
+
import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-peGVtLxi.js";
|
|
4
4
|
const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
|
|
5
5
|
class WorkerManager {
|
|
6
6
|
constructor(defaults) {
|