@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.
Files changed (35) hide show
  1. package/out/headless/index.mjs +320 -44
  2. package/out/headless/index.mjs.map +4 -4
  3. package/out/main/index.js +275 -15
  4. package/out/preload/index.js +3 -0
  5. package/out/renderer/assets/{cssMode-D3kH1Kju.js → cssMode-BW-SuYuP.js} +3 -3
  6. package/out/renderer/assets/{freemarker2-BCHZUSLb.js → freemarker2-2YWYzawi.js} +1 -1
  7. package/out/renderer/assets/{handlebars-DKx-Fw-H.js → handlebars-EwtUQRsf.js} +1 -1
  8. package/out/renderer/assets/{html-BSCM04uL.js → html-BNZkIDb9.js} +1 -1
  9. package/out/renderer/assets/{htmlMode-BucU1MUc.js → htmlMode-C2dZKrOy.js} +3 -3
  10. package/out/renderer/assets/{index-BsdOeO0U.js → index-Bm_rbVP-.js} +114 -34
  11. package/out/renderer/assets/{index-BzF7I1my.css → index-CrTu3Z4M.css} +21 -0
  12. package/out/renderer/assets/{javascript-bPY5C4uq.js → javascript-busdVZMv.js} +2 -2
  13. package/out/renderer/assets/{jsonMode-BmJotb6E.js → jsonMode-BaVI6jAw.js} +3 -3
  14. package/out/renderer/assets/{liquid-Cja_Pzh3.js → liquid-DG08un1Q.js} +1 -1
  15. package/out/renderer/assets/{lspLanguageFeatures-hoVZfVKv.js → lspLanguageFeatures-peGVtLxi.js} +1 -1
  16. package/out/renderer/assets/{mdx-C0s81MOq.js → mdx-DogBhUxZ.js} +1 -1
  17. package/out/renderer/assets/{python-CulkBOJr.js → python-Bf-INYXh.js} +1 -1
  18. package/out/renderer/assets/{razor-czmzhwVZ.js → razor-DLrZ2hsF.js} +1 -1
  19. package/out/renderer/assets/{tsMode-B90EqYGx.js → tsMode-B4oEmliC.js} +1 -1
  20. package/out/renderer/assets/{typescript-Ckc6emP2.js → typescript-CjkgfhVK.js} +1 -1
  21. package/out/renderer/assets/{xml-CKh-JyGN.js → xml-0FAXmuVg.js} +1 -1
  22. package/out/renderer/assets/{yaml-B49zLim4.js → yaml-DWxnPuy8.js} +1 -1
  23. package/out/renderer/index.html +2 -2
  24. package/package.json +1 -1
  25. package/src/main/ctlsurfApi.ts +26 -0
  26. package/src/main/headless.ts +33 -28
  27. package/src/main/index.ts +8 -0
  28. package/src/main/orchestrator.ts +63 -2
  29. package/src/main/timeTracker.ts +223 -0
  30. package/src/main/tui.ts +25 -5
  31. package/src/preload/index.ts +7 -1
  32. package/src/renderer/App.tsx +36 -0
  33. package/src/renderer/components/SettingsDialog.tsx +38 -1
  34. package/src/renderer/components/TerminalPanel.tsx +25 -13
  35. 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$2(...args) {
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$2("[worker-ws] No API key or registration, skipping connect");
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$2("[worker-ws] shouldReconnect is false, aborting connect");
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$2(`[worker-ws] Connecting to ${url.replace(/token=.*/, "token=***")}...`);
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$2("[worker-ws] Failed to create WebSocket:", err);
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$2("[worker-ws] Connected, sending register");
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$2("[worker-ws] Failed to parse message:", err);
4354
+ log$3("[worker-ws] Failed to parse message:", err);
4336
4355
  }
4337
4356
  };
4338
4357
  this.ws.onclose = (event) => {
4339
- log$2(`[worker-ws] Disconnected: ${event.code} ${event.reason}`);
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$2("[worker-ws] WebSocket error");
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$2("[worker-ws] Worker approved!");
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;
@@ -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-BsdOeO0U.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-hoVZfVKv.js";
3
- import { h, i, j, t, k } from "./lspLanguageFeatures-hoVZfVKv.js";
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,4 +1,4 @@
1
- import { l as languages } from "./index-BsdOeO0U.js";
1
+ import { l as languages } from "./index-Bm_rbVP-.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "assign",
4
4
  "flush",
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-BsdOeO0U.js";
1
+ import { l as languages } from "./index-Bm_rbVP-.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "area",
4
4
  "base",
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-BsdOeO0U.js";
1
+ import { l as languages } from "./index-Bm_rbVP-.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "area",
4
4
  "base",
@@ -1,6 +1,6 @@
1
- import { c as createWebWorker, l as languages } from "./index-BsdOeO0U.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-hoVZfVKv.js";
3
- import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-hoVZfVKv.js";
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) {