@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.
- 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-DiOmyihM.js} +3 -3
- package/out/renderer/assets/{freemarker2-BCHZUSLb.js → freemarker2-BAfv60yb.js} +1 -1
- package/out/renderer/assets/{handlebars-DKx-Fw-H.js → handlebars-Ult17NzQ.js} +1 -1
- package/out/renderer/assets/{html-BSCM04uL.js → html-DCxh4J-1.js} +1 -1
- package/out/renderer/assets/{htmlMode-BucU1MUc.js → htmlMode-CQ5Xenrg.js} +3 -3
- package/out/renderer/assets/{index-BsdOeO0U.js → index-BnCJ1IaZ.js} +106 -28
- package/out/renderer/assets/{index-BzF7I1my.css → index-CrTu3Z4M.css} +21 -0
- package/out/renderer/assets/{javascript-bPY5C4uq.js → javascript-U5dsRcHx.js} +2 -2
- package/out/renderer/assets/{jsonMode-BmJotb6E.js → jsonMode-DshPNyVy.js} +3 -3
- package/out/renderer/assets/{liquid-Cja_Pzh3.js → liquid-jHHLYTlB.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-hoVZfVKv.js → lspLanguageFeatures-CUafmPGy.js} +1 -1
- package/out/renderer/assets/{mdx-C0s81MOq.js → mdx-Ct-tiY6g.js} +1 -1
- package/out/renderer/assets/{python-CulkBOJr.js → python-wD3UwKPV.js} +1 -1
- package/out/renderer/assets/{razor-czmzhwVZ.js → razor-11ECS4oH.js} +1 -1
- package/out/renderer/assets/{tsMode-B90EqYGx.js → tsMode-D-7JexQ_.js} +1 -1
- package/out/renderer/assets/{typescript-Ckc6emP2.js → typescript-Cvna1mak.js} +1 -1
- package/out/renderer/assets/{xml-CKh-JyGN.js → xml-JsEaImjA.js} +1 -1
- package/out/renderer/assets/{yaml-B49zLim4.js → yaml-B8pCNDb_.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 +14 -6
- package/src/renderer/styles.css +21 -0
package/out/headless/index.mjs
CHANGED
|
@@ -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
|
|
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/
|
|
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
|
-
|
|
824
|
+
log3(`[worker-ws] Status: ${status}`);
|
|
608
825
|
events.onWorkerStatus(status);
|
|
609
826
|
},
|
|
610
827
|
onMessage: (message) => {
|
|
611
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1131
|
+
log3("[worker-ws] No API key, skipping WS connect");
|
|
874
1132
|
return;
|
|
875
1133
|
}
|
|
876
1134
|
this.workerWs.connect({
|
|
877
|
-
machine:
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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(() => {
|