@phenx-inc/ctlsurf 0.2.0 → 0.3.0

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-DiOmyihM.js} +3 -3
  6. package/out/renderer/assets/{freemarker2-BCHZUSLb.js → freemarker2-BAfv60yb.js} +1 -1
  7. package/out/renderer/assets/{handlebars-DKx-Fw-H.js → handlebars-Ult17NzQ.js} +1 -1
  8. package/out/renderer/assets/{html-BSCM04uL.js → html-DCxh4J-1.js} +1 -1
  9. package/out/renderer/assets/{htmlMode-BucU1MUc.js → htmlMode-CQ5Xenrg.js} +3 -3
  10. package/out/renderer/assets/{index-BsdOeO0U.js → index-BnCJ1IaZ.js} +106 -28
  11. package/out/renderer/assets/{index-BzF7I1my.css → index-CrTu3Z4M.css} +21 -0
  12. package/out/renderer/assets/{javascript-bPY5C4uq.js → javascript-U5dsRcHx.js} +2 -2
  13. package/out/renderer/assets/{jsonMode-BmJotb6E.js → jsonMode-DshPNyVy.js} +3 -3
  14. package/out/renderer/assets/{liquid-Cja_Pzh3.js → liquid-jHHLYTlB.js} +1 -1
  15. package/out/renderer/assets/{lspLanguageFeatures-hoVZfVKv.js → lspLanguageFeatures-CUafmPGy.js} +1 -1
  16. package/out/renderer/assets/{mdx-C0s81MOq.js → mdx-Ct-tiY6g.js} +1 -1
  17. package/out/renderer/assets/{python-CulkBOJr.js → python-wD3UwKPV.js} +1 -1
  18. package/out/renderer/assets/{razor-czmzhwVZ.js → razor-11ECS4oH.js} +1 -1
  19. package/out/renderer/assets/{tsMode-B90EqYGx.js → tsMode-D-7JexQ_.js} +1 -1
  20. package/out/renderer/assets/{typescript-Ckc6emP2.js → typescript-Cvna1mak.js} +1 -1
  21. package/out/renderer/assets/{xml-CKh-JyGN.js → xml-JsEaImjA.js} +1 -1
  22. package/out/renderer/assets/{yaml-B49zLim4.js → yaml-B8pCNDb_.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 +14 -6
  35. package/src/renderer/styles.css +21 -0
@@ -37,7 +37,7 @@ var require_electron = __commonJS({
37
37
  // src/main/orchestrator.ts
38
38
  import path from "path";
39
39
  import fs from "fs";
40
- import os2 from "os";
40
+ import os3 from "os";
41
41
 
42
42
  // src/main/pty.ts
43
43
  import { createRequire } from "module";
@@ -198,6 +198,25 @@ var CtlsurfApi = class {
198
198
  const folder = await this.request("GET", `/folders/${folderId}`);
199
199
  return folder?.pages || [];
200
200
  }
201
+ async getFolder(folderId) {
202
+ return this.request("GET", `/folders/${folderId}`);
203
+ }
204
+ // ─── Datastore ───────────────────────────────────────
205
+ async getPageBlockSummaries(pageId) {
206
+ return this.request("GET", `/blocks/page/${pageId}/summary`);
207
+ }
208
+ async addRow(blockId, data) {
209
+ return this.request("POST", `/datastore/${blockId}/rows`, { data });
210
+ }
211
+ async updateRow(blockId, rowId, data) {
212
+ return this.request("PUT", `/datastore/${blockId}/rows/${rowId}`, { data });
213
+ }
214
+ async getDatastoreSchema(blockId) {
215
+ return this.request("GET", `/datastore/${blockId}/schema`);
216
+ }
217
+ async updateDatastoreSchema(blockId, columns) {
218
+ return this.request("PUT", `/datastore/${blockId}/schema`, { columns });
219
+ }
201
220
  async findFolderByGitRemote(gitRemote) {
202
221
  const folders = await this.request("GET", "/folders");
203
222
  return folders?.find((f) => f.git_remote === gitRemote || f.root_path === gitRemote) || null;
@@ -567,19 +586,216 @@ var WorkerWsClient = class {
567
586
  }
568
587
  };
569
588
 
570
- // src/main/orchestrator.ts
589
+ // src/main/timeTracker.ts
590
+ import os2 from "os";
591
+ import { randomUUID } from "crypto";
592
+ var DATASTORE_TITLE = "Time Tracking";
593
+ var AGENT_DATASTORE_PAGE_TITLE = "Agent Datastore";
594
+ var FIRST_CHECKPOINT_DELAY_MS = 30 * 1e3;
595
+ var CHECKPOINT_INTERVAL_MS = 5 * 60 * 1e3;
596
+ var COLUMNS = [
597
+ { name: "Started", type: "date" },
598
+ { name: "Active Time", type: "number" },
599
+ { name: "Agent", type: "text" },
600
+ { name: "Worker", type: "text" },
601
+ { name: "Session", type: "text" },
602
+ { name: "Notes", type: "text" }
603
+ ];
571
604
  function log2(...args) {
605
+ try {
606
+ console.log("[time-tracker]", ...args);
607
+ } catch {
608
+ }
609
+ }
610
+ function findPageByTitle(pages, title) {
611
+ for (const p of pages) {
612
+ if (p?.title === title) return p;
613
+ if (p?.children?.length) {
614
+ const c = findPageByTitle(p.children, title);
615
+ if (c) return c;
616
+ }
617
+ }
618
+ return null;
619
+ }
620
+ var TimeTracker = class {
621
+ api;
622
+ sessions = /* @__PURE__ */ new Map();
623
+ blockCache = /* @__PURE__ */ new Map();
624
+ constructor(api) {
625
+ this.api = api;
626
+ }
627
+ async startSession(tabId, cwd, agentName, idleTimeoutMin) {
628
+ if (this.sessions.has(tabId)) {
629
+ await this.endSession(tabId);
630
+ }
631
+ try {
632
+ const blockId = await this.ensureDatastore(cwd);
633
+ if (!blockId) {
634
+ log2(`No "${AGENT_DATASTORE_PAGE_TITLE}" page found for ${cwd} \u2014 tracking disabled for this session`);
635
+ return;
636
+ }
637
+ const startedAt = Date.now();
638
+ const startedIso = new Date(startedAt).toISOString();
639
+ const sessionUuid = randomUUID();
640
+ const row = await this.api.addRow(blockId, {
641
+ Started: startedIso,
642
+ "Active Time": 0,
643
+ Agent: agentName,
644
+ Worker: os2.hostname(),
645
+ Session: sessionUuid,
646
+ Notes: ""
647
+ });
648
+ const rowId = row?.id;
649
+ if (!rowId) {
650
+ log2("addRow returned no id; aborting tracking", row);
651
+ return;
652
+ }
653
+ const state = {
654
+ blockId,
655
+ rowId,
656
+ cwd,
657
+ startedAt,
658
+ lastActivity: startedAt,
659
+ activeMs: 0,
660
+ idleTimeoutMs: Math.max(1, idleTimeoutMin) * 60 * 1e3,
661
+ firstCheckpointTimer: null,
662
+ checkpointTimer: null,
663
+ ended: false
664
+ };
665
+ state.firstCheckpointTimer = setTimeout(() => {
666
+ void this.checkpoint(tabId);
667
+ const live = this.sessions.get(tabId);
668
+ if (live && !live.ended) {
669
+ live.checkpointTimer = setInterval(() => {
670
+ void this.checkpoint(tabId);
671
+ }, CHECKPOINT_INTERVAL_MS);
672
+ }
673
+ }, FIRST_CHECKPOINT_DELAY_MS);
674
+ this.sessions.set(tabId, state);
675
+ log2(`Started tracking tab=${tabId} agent="${agentName}" cwd=${cwd}`);
676
+ } catch (err) {
677
+ log2(`startSession failed: ${err?.message || err}`);
678
+ }
679
+ }
680
+ isTracking(tabId) {
681
+ const s = this.sessions.get(tabId);
682
+ return !!s && !s.ended;
683
+ }
684
+ recordActivity(tabId) {
685
+ const s = this.sessions.get(tabId);
686
+ if (!s || s.ended) return;
687
+ const now = Date.now();
688
+ const delta = now - s.lastActivity;
689
+ if (delta < s.idleTimeoutMs) {
690
+ s.activeMs += delta;
691
+ }
692
+ s.lastActivity = now;
693
+ }
694
+ async endSession(tabId) {
695
+ const s = this.sessions.get(tabId);
696
+ if (!s || s.ended) return;
697
+ s.ended = true;
698
+ if (s.firstCheckpointTimer) clearTimeout(s.firstCheckpointTimer);
699
+ if (s.checkpointTimer) clearInterval(s.checkpointTimer);
700
+ try {
701
+ await this.writeRow(s, Date.now());
702
+ } catch (err) {
703
+ log2(`endSession write failed: ${err?.message || err}`);
704
+ }
705
+ this.sessions.delete(tabId);
706
+ }
707
+ async endAll() {
708
+ const ids = [...this.sessions.keys()];
709
+ await Promise.all(ids.map((id) => this.endSession(id)));
710
+ }
711
+ async checkpoint(tabId) {
712
+ const s = this.sessions.get(tabId);
713
+ if (!s || s.ended) return;
714
+ try {
715
+ await this.writeRow(s, Date.now());
716
+ } catch (err) {
717
+ log2(`checkpoint failed: ${err?.message || err}`);
718
+ }
719
+ }
720
+ async writeRow(s, _endTimeMs) {
721
+ const activeMin = Math.round(s.activeMs / 6e4);
722
+ await this.api.updateRow(s.blockId, s.rowId, {
723
+ "Active Time": activeMin
724
+ });
725
+ }
726
+ async ensureDatastore(cwd) {
727
+ const cached = this.blockCache.get(cwd);
728
+ if (cached) return cached;
729
+ let folder = null;
730
+ try {
731
+ folder = await this.api.findFolderByPath(cwd);
732
+ } catch {
733
+ return null;
734
+ }
735
+ if (!folder?.id) return null;
736
+ const folderDetail = await this.api.getFolder(folder.id);
737
+ const agentPage = findPageByTitle(folderDetail?.pages || [], AGENT_DATASTORE_PAGE_TITLE);
738
+ if (!agentPage?.id) return null;
739
+ const summaries = await this.api.getPageBlockSummaries(agentPage.id);
740
+ const existing = (summaries || []).find((b) => b?.type === "datastore" && b?.title === DATASTORE_TITLE);
741
+ if (existing?.id) {
742
+ await this.ensureColumns(existing.id);
743
+ this.blockCache.set(cwd, existing.id);
744
+ return existing.id;
745
+ }
746
+ const columns = COLUMNS.map((c, i) => ({ id: `col_${i}`, name: c.name, type: c.type }));
747
+ const created = await this.api.createBlock(agentPage.id, {
748
+ type: "datastore",
749
+ title: DATASTORE_TITLE,
750
+ props: { columns }
751
+ });
752
+ if (created?.id) {
753
+ log2(`Created "${DATASTORE_TITLE}" datastore on Agent Datastore page for ${cwd}`);
754
+ this.blockCache.set(cwd, created.id);
755
+ return created.id;
756
+ }
757
+ return null;
758
+ }
759
+ async ensureColumns(blockId) {
760
+ try {
761
+ const schema = await this.api.getDatastoreSchema(blockId);
762
+ const existingCols = schema.columns || [];
763
+ const existingNames = new Set(existingCols.map((c) => c.name));
764
+ const missing = COLUMNS.filter((c) => !existingNames.has(c.name));
765
+ if (missing.length === 0) return;
766
+ const usedIds = new Set(existingCols.map((c) => c.id));
767
+ let nextIdx = existingCols.length;
768
+ const appended = missing.map((c) => {
769
+ let id = `col_${nextIdx++}`;
770
+ while (usedIds.has(id)) id = `col_${nextIdx++}`;
771
+ usedIds.add(id);
772
+ return { id, name: c.name, type: c.type };
773
+ });
774
+ const merged = [...existingCols, ...appended];
775
+ await this.api.updateDatastoreSchema(blockId, merged);
776
+ log2(`Added ${missing.length} missing column(s) to existing Time Tracking datastore: ${missing.map((c) => c.name).join(", ")}`);
777
+ } catch (err) {
778
+ log2(`ensureColumns failed: ${err?.message || err}`);
779
+ }
780
+ }
781
+ };
782
+
783
+ // src/main/orchestrator.ts
784
+ function log3(...args) {
572
785
  try {
573
786
  console.log(...args);
574
787
  } catch {
575
788
  }
576
789
  }
790
+ var DEFAULT_IDLE_TIMEOUT_MIN = 15;
577
791
  var DEFAULT_PROFILES = {
578
792
  production: {
579
793
  name: "Production",
580
794
  apiKey: "",
581
795
  baseUrl: "https://app.ctlsurf.com",
582
- dataspacePageId: ""
796
+ dataspacePageId: "",
797
+ trackTime: true,
798
+ idleTimeoutMin: 15
583
799
  }
584
800
  };
585
801
  var TERM_STREAM_INTERVAL_MS = 50;
@@ -590,6 +806,7 @@ var Orchestrator = class {
590
806
  ctlsurfApi = new CtlsurfApi();
591
807
  bridge = new ConversationBridge();
592
808
  workerWs;
809
+ timeTracker = new TimeTracker(this.ctlsurfApi);
593
810
  // State
594
811
  tabs = /* @__PURE__ */ new Map();
595
812
  activeTabId = null;
@@ -604,11 +821,11 @@ var Orchestrator = class {
604
821
  this.events = events;
605
822
  this.workerWs = new WorkerWsClient({
606
823
  onStatusChange: (status) => {
607
- log2(`[worker-ws] Status: ${status}`);
824
+ log3(`[worker-ws] Status: ${status}`);
608
825
  events.onWorkerStatus(status);
609
826
  },
610
827
  onMessage: (message) => {
611
- log2(`[worker-ws] Incoming message: ${message.id} (${message.type})`);
828
+ log3(`[worker-ws] Incoming message: ${message.id} (${message.type})`);
612
829
  events.onWorkerMessage(message);
613
830
  this.workerWs.sendAck(message.id);
614
831
  if (message.type === "prompt" || message.type === "task_dispatch") {
@@ -620,7 +837,7 @@ var Orchestrator = class {
620
837
  }
621
838
  },
622
839
  onRegistered: (data) => {
623
- log2(`[worker-ws] Registered: worker_id=${data.worker_id}, folder_id=${data.folder_id}, status=${data.status}`);
840
+ log3(`[worker-ws] Registered: worker_id=${data.worker_id}, folder_id=${data.folder_id}, status=${data.status}`);
624
841
  events.onWorkerRegistered(data);
625
842
  if (!data.folder_id) {
626
843
  events.onWorkerStatus("no_project");
@@ -658,7 +875,7 @@ var Orchestrator = class {
658
875
  const baseUrl = profile.baseUrl || process.env.CTLSURF_BASE_URL || "https://app.ctlsurf.com";
659
876
  this.ctlsurfApi.setBaseUrl(baseUrl);
660
877
  this.workerWs.setBaseUrl(baseUrl);
661
- log2(`[settings] Profile applied: ${profile.name} (${baseUrl})`);
878
+ log3(`[settings] Profile applied: ${profile.name} (${baseUrl})`);
662
879
  }
663
880
  loadSettings() {
664
881
  try {
@@ -682,7 +899,7 @@ var Orchestrator = class {
682
899
  }
683
900
  };
684
901
  this.saveSettings();
685
- log2("[settings] Migrated legacy settings to profiles");
902
+ log3("[settings] Migrated legacy settings to profiles");
686
903
  } else {
687
904
  this.settings = raw;
688
905
  if (!this.settings.profiles.production) {
@@ -704,7 +921,7 @@ var Orchestrator = class {
704
921
  fs.mkdirSync(this.settingsDir, { recursive: true });
705
922
  fs.writeFileSync(settingsPath, JSON.stringify(this.settings, null, 2));
706
923
  } catch (err) {
707
- log2("[settings] Failed to save:", err.message);
924
+ log3("[settings] Failed to save:", err.message);
708
925
  }
709
926
  }
710
927
  overrideApiKey(key) {
@@ -724,7 +941,9 @@ var Orchestrator = class {
724
941
  name: p.name,
725
942
  baseUrl: p.baseUrl,
726
943
  hasApiKey: !!p.apiKey,
727
- dataspacePageId: p.dataspacePageId || null
944
+ dataspacePageId: p.dataspacePageId || null,
945
+ trackTime: p.trackTime !== false,
946
+ idleTimeoutMin: p.idleTimeoutMin ?? DEFAULT_IDLE_TIMEOUT_MIN
728
947
  }))
729
948
  };
730
949
  }
@@ -736,7 +955,9 @@ var Orchestrator = class {
736
955
  name: p.name,
737
956
  baseUrl: p.baseUrl,
738
957
  hasApiKey: !!p.apiKey,
739
- dataspacePageId: p.dataspacePageId || ""
958
+ dataspacePageId: p.dataspacePageId || "",
959
+ trackTime: p.trackTime !== false,
960
+ idleTimeoutMin: p.idleTimeoutMin ?? DEFAULT_IDLE_TIMEOUT_MIN
740
961
  };
741
962
  }
742
963
  saveProfile(profileId, data) {
@@ -745,7 +966,9 @@ var Orchestrator = class {
745
966
  name: data.name,
746
967
  apiKey: data.apiKey !== void 0 ? data.apiKey : existing?.apiKey || "",
747
968
  baseUrl: data.baseUrl || "https://app.ctlsurf.com",
748
- dataspacePageId: data.dataspacePageId || ""
969
+ dataspacePageId: data.dataspacePageId || "",
970
+ trackTime: data.trackTime !== void 0 ? data.trackTime : existing?.trackTime !== false,
971
+ idleTimeoutMin: data.idleTimeoutMin !== void 0 ? data.idleTimeoutMin : existing?.idleTimeoutMin ?? DEFAULT_IDLE_TIMEOUT_MIN
749
972
  };
750
973
  this.saveSettings();
751
974
  if (profileId === this.settings.activeProfile) {
@@ -783,7 +1006,7 @@ var Orchestrator = class {
783
1006
  return { ok: true };
784
1007
  }
785
1008
  // ─── PTY & Agent (multi-tab) ─────────────────────
786
- async spawnAgent(tabId, agent, cwd) {
1009
+ async spawnAgent(tabId, agent, cwd, opts) {
787
1010
  const existing = this.tabs.get(tabId);
788
1011
  if (existing) {
789
1012
  if (existing.termStreamTimer) clearTimeout(existing.termStreamTimer);
@@ -802,6 +1025,7 @@ var Orchestrator = class {
802
1025
  this.tabs.set(tabId, tab);
803
1026
  ptyManager.onData((data) => {
804
1027
  this.events.onPtyData(tabId, data);
1028
+ this.timeTracker.recordActivity(tabId);
805
1029
  if (tabId === this.activeTabId) {
806
1030
  this.bridge.feedOutput(data);
807
1031
  this.streamTerminalData(tabId, data);
@@ -809,6 +1033,7 @@ var Orchestrator = class {
809
1033
  });
810
1034
  ptyManager.onExit(async (exitCode) => {
811
1035
  this.events.onPtyExit(tabId, exitCode);
1036
+ await this.timeTracker.endSession(tabId);
812
1037
  if (tabId === this.activeTabId) {
813
1038
  this.bridge.endSession();
814
1039
  if (this.currentAgent && isCodingAgent(this.currentAgent)) {
@@ -819,6 +1044,16 @@ var Orchestrator = class {
819
1044
  if (t?.termStreamTimer) clearTimeout(t.termStreamTimer);
820
1045
  });
821
1046
  this.bridge.startSession();
1047
+ const profile = this.getActiveProfile();
1048
+ const shouldTrack = opts?.trackTime !== void 0 ? opts.trackTime : profile.trackTime !== false;
1049
+ if (shouldTrack) {
1050
+ void this.timeTracker.startSession(
1051
+ tabId,
1052
+ cwd,
1053
+ agent.name,
1054
+ profile.idleTimeoutMin ?? DEFAULT_IDLE_TIMEOUT_MIN
1055
+ );
1056
+ }
822
1057
  if (isCodingAgent(agent)) {
823
1058
  this.connectWorkerWs(agent, cwd);
824
1059
  } else {
@@ -843,6 +1078,7 @@ var Orchestrator = class {
843
1078
  const tab = this.tabs.get(tabId);
844
1079
  if (!tab) return;
845
1080
  if (tab.termStreamTimer) clearTimeout(tab.termStreamTimer);
1081
+ await this.timeTracker.endSession(tabId);
846
1082
  tab.ptyManager.kill();
847
1083
  this.tabs.delete(tabId);
848
1084
  if (tabId === this.activeTabId) {
@@ -865,16 +1101,38 @@ var Orchestrator = class {
865
1101
  getTabIds() {
866
1102
  return [...this.tabs.keys()];
867
1103
  }
1104
+ // ─── Tracking control (active tab) ──────────────
1105
+ isActiveTabTracking() {
1106
+ if (!this.activeTabId) return false;
1107
+ return this.timeTracker.isTracking(this.activeTabId);
1108
+ }
1109
+ async setActiveTabTracking(enabled) {
1110
+ if (!this.activeTabId) return;
1111
+ const tab = this.tabs.get(this.activeTabId);
1112
+ if (!tab) return;
1113
+ if (enabled) {
1114
+ if (this.timeTracker.isTracking(this.activeTabId)) return;
1115
+ const profile = this.getActiveProfile();
1116
+ await this.timeTracker.startSession(
1117
+ this.activeTabId,
1118
+ tab.cwd,
1119
+ tab.agent.name,
1120
+ profile.idleTimeoutMin ?? DEFAULT_IDLE_TIMEOUT_MIN
1121
+ );
1122
+ } else {
1123
+ await this.timeTracker.endSession(this.activeTabId);
1124
+ }
1125
+ }
868
1126
  // ─── Worker WebSocket ───────────────────────────
869
1127
  connectWorkerWs(agent, cwd) {
870
1128
  const profile = this.getActiveProfile();
871
1129
  const apiKey = profile.apiKey || process.env.CTLSURF_API_KEY;
872
1130
  if (!apiKey) {
873
- log2("[worker-ws] No API key, skipping WS connect");
1131
+ log3("[worker-ws] No API key, skipping WS connect");
874
1132
  return;
875
1133
  }
876
1134
  this.workerWs.connect({
877
- machine: os2.hostname(),
1135
+ machine: os3.hostname(),
878
1136
  cwd,
879
1137
  agent: agent.name
880
1138
  });
@@ -910,6 +1168,7 @@ var Orchestrator = class {
910
1168
  // ─── Shutdown ───────────────────────────────────
911
1169
  async shutdown() {
912
1170
  this.bridge.endSession();
1171
+ await this.timeTracker.endAll();
913
1172
  for (const [, tab] of this.tabs) {
914
1173
  if (tab.termStreamTimer) clearTimeout(tab.termStreamTimer);
915
1174
  tab.ptyManager.kill();
@@ -921,17 +1180,17 @@ var Orchestrator = class {
921
1180
 
922
1181
  // src/main/settingsDir.ts
923
1182
  import path2 from "path";
924
- import os3 from "os";
1183
+ import os4 from "os";
925
1184
  function getSettingsDir(useElectron) {
926
1185
  if (useElectron) {
927
1186
  const { app } = require_electron();
928
1187
  return app.getPath("userData");
929
1188
  }
930
1189
  if (process.platform === "darwin") {
931
- return path2.join(os3.homedir(), "Library", "Application Support", "ctlsurf-worker");
1190
+ return path2.join(os4.homedir(), "Library", "Application Support", "ctlsurf-worker");
932
1191
  }
933
1192
  return path2.join(
934
- process.env.XDG_CONFIG_HOME || path2.join(os3.homedir(), ".config"),
1193
+ process.env.XDG_CONFIG_HOME || path2.join(os4.homedir(), ".config"),
935
1194
  "ctlsurf-worker"
936
1195
  );
937
1196
  }
@@ -1038,11 +1297,12 @@ var Tui = class {
1038
1297
  * Show an interactive agent picker modal.
1039
1298
  * Uses alternate screen just for the picker, then exits back to normal.
1040
1299
  */
1041
- showAgentPicker(agents) {
1300
+ showAgentPicker(agents, options) {
1042
1301
  return new Promise((resolve) => {
1043
1302
  let selected = 0;
1303
+ let trackTime = options.initialTrackTime;
1044
1304
  const modalWidth = 44;
1045
- const modalHeight = agents.length + 4;
1305
+ const modalHeight = agents.length + 4 + 2;
1046
1306
  const startCol = Math.max(1, Math.floor((this.cols - modalWidth) / 2));
1047
1307
  const startRow = Math.max(1, Math.floor((this.rows - modalHeight) / 2));
1048
1308
  this.write(`${CSI}?1049h`);
@@ -1076,9 +1336,19 @@ var Tui = class {
1076
1336
  const pad = " ".repeat(Math.max(0, modalWidth - 2 - contentLen));
1077
1337
  this.write(`${CSI}${row};${startCol}H${bg}${FG_DIM}\u2502${RESET}${bg}${content}${pad}${RESET}${BG_MODAL}${FG_DIM}\u2502${RESET}`);
1078
1338
  }
1079
- const botRow = startRow + 3 + agents.length;
1339
+ const innerSep = "\u251C" + "\u2500".repeat(modalWidth - 2) + "\u2524";
1340
+ const sepRow = startRow + 3 + agents.length;
1341
+ this.write(`${CSI}${sepRow};${startCol}H${BG_MODAL}${FG_DIM}${innerSep}${RESET}`);
1342
+ const trackRow = sepRow + 1;
1343
+ const checkbox = trackTime ? `${FG_GREEN}[\u2713]${RESET}${BG_MODAL}` : `${FG_DIM}[ ]${RESET}${BG_MODAL}`;
1344
+ const trackLabelFg = trackTime ? FG_WHITE : FG_DIM;
1345
+ const trackContent = ` ${checkbox} ${trackLabelFg}Track time${RESET}${BG_MODAL}`;
1346
+ const trackContentLen = 2 + 3 + 1 + "Track time".length;
1347
+ const trackPad = " ".repeat(Math.max(0, modalWidth - 2 - trackContentLen));
1348
+ this.write(`${CSI}${trackRow};${startCol}H${BG_MODAL}${FG_DIM}\u2502${RESET}${BG_MODAL}${trackContent}${trackPad}${FG_DIM}\u2502${RESET}`);
1349
+ const botRow = trackRow + 1;
1080
1350
  this.write(`${CSI}${botRow};${startCol}H${BG_MODAL}${FG_DIM}${botBorder}${RESET}`);
1081
- const hint = "\u2191\u2193 navigate \xB7 Enter select \xB7 q quit";
1351
+ const hint = "\u2191\u2193 navigate \xB7 Enter select \xB7 t track \xB7 q quit";
1082
1352
  const hintCol = Math.max(1, Math.floor((this.cols - hint.length) / 2));
1083
1353
  this.write(`${CSI}${botRow + 2};${hintCol}H${FG_DIM}${hint}${RESET}`);
1084
1354
  };
@@ -1095,9 +1365,12 @@ var Tui = class {
1095
1365
  } else if (key === "\x1B[B" || key === "j") {
1096
1366
  selected = (selected + 1) % agents.length;
1097
1367
  drawModal();
1368
+ } else if (key === "t" || key === "T" || key === " ") {
1369
+ trackTime = !trackTime;
1370
+ drawModal();
1098
1371
  } else if (key === "\r" || key === "\n") {
1099
1372
  cleanup();
1100
- resolve(selected);
1373
+ resolve({ agentIdx: selected, trackTime });
1101
1374
  } else if (key === "q" || key === "\x1B" || key === "") {
1102
1375
  cleanup();
1103
1376
  this.write(`${CSI}?25h`);
@@ -1221,26 +1494,6 @@ async function main() {
1221
1494
  const settingsDir = getSettingsDir(false);
1222
1495
  const tui = new Tui();
1223
1496
  const agents = getBuiltinAgents();
1224
- let agent;
1225
- if (args.agent) {
1226
- const found = agents.find((a) => a.id === args.agent);
1227
- agent = found || {
1228
- id: args.agent,
1229
- name: args.agent,
1230
- command: args.agent,
1231
- args: [],
1232
- description: `Custom agent: ${args.agent}`
1233
- };
1234
- } else {
1235
- const selectedIdx = await tui.showAgentPicker(agents);
1236
- agent = agents[selectedIdx];
1237
- }
1238
- tui.update({
1239
- agentName: agent.name,
1240
- cwd: args.cwd,
1241
- mode: "terminal"
1242
- });
1243
- tui.init();
1244
1497
  const orchestrator = new Orchestrator(settingsDir, {
1245
1498
  onPtyData: (_tabId, data) => {
1246
1499
  tui.writePtyData(data);
@@ -1266,9 +1519,32 @@ async function main() {
1266
1519
  if (args.profile) orchestrator.switchProfile(args.profile);
1267
1520
  if (args.apiKey) orchestrator.overrideApiKey(args.apiKey);
1268
1521
  if (args.baseUrl) orchestrator.overrideBaseUrl(args.baseUrl);
1522
+ let agent;
1523
+ let trackTimeOverride;
1524
+ if (args.agent) {
1525
+ const found = agents.find((a) => a.id === args.agent);
1526
+ agent = found || {
1527
+ id: args.agent,
1528
+ name: args.agent,
1529
+ command: args.agent,
1530
+ args: [],
1531
+ description: `Custom agent: ${args.agent}`
1532
+ };
1533
+ } else {
1534
+ const initialTrackTime = orchestrator.getActiveProfile().trackTime !== false;
1535
+ const picked = await tui.showAgentPicker(agents, { initialTrackTime });
1536
+ agent = agents[picked.agentIdx];
1537
+ trackTimeOverride = picked.trackTime;
1538
+ }
1539
+ tui.update({
1540
+ agentName: agent.name,
1541
+ cwd: args.cwd,
1542
+ mode: "terminal"
1543
+ });
1544
+ tui.init();
1269
1545
  const HEADLESS_TAB = "headless";
1270
1546
  const ptySize = tui.getPtySize();
1271
- await orchestrator.spawnAgent(HEADLESS_TAB, agent, args.cwd);
1547
+ await orchestrator.spawnAgent(HEADLESS_TAB, agent, args.cwd, { trackTime: trackTimeOverride });
1272
1548
  orchestrator.resizePty(HEADLESS_TAB, ptySize.cols, ptySize.rows);
1273
1549
  if (isCodingAgent(agent)) {
1274
1550
  setTimeout(() => {