@phenx-inc/ctlsurf 0.1.21 → 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 +409 -99
- package/out/headless/index.mjs.map +4 -4
- package/out/main/index.js +419 -77
- package/out/preload/index.js +12 -8
- package/out/renderer/assets/{cssMode-C6bY9C4O.js → cssMode-DiOmyihM.js} +3 -3
- package/out/renderer/assets/{freemarker2-CkAJiX1K.js → freemarker2-BAfv60yb.js} +1 -1
- package/out/renderer/assets/{handlebars-DnLXVUXp.js → handlebars-Ult17NzQ.js} +1 -1
- package/out/renderer/assets/{html-Ds5-qvDh.js → html-DCxh4J-1.js} +1 -1
- package/out/renderer/assets/{htmlMode-DYFYy4MK.js → htmlMode-CQ5Xenrg.js} +3 -3
- package/out/renderer/assets/{index-DwSsD_Xm.js → index-BnCJ1IaZ.js} +308 -101
- package/out/renderer/assets/{index-DK9wLFFm.css → index-CrTu3Z4M.css} +132 -0
- package/out/renderer/assets/{javascript-CiHhG2a9.js → javascript-U5dsRcHx.js} +2 -2
- package/out/renderer/assets/{jsonMode-DdDRlbXP.js → jsonMode-DshPNyVy.js} +3 -3
- package/out/renderer/assets/{liquid-BP5mb-uD.js → liquid-jHHLYTlB.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-Dljhj5Gh.js → lspLanguageFeatures-CUafmPGy.js} +1 -1
- package/out/renderer/assets/{mdx-D4u3N7dt.js → mdx-Ct-tiY6g.js} +1 -1
- package/out/renderer/assets/{python-BQDHXVwp.js → python-wD3UwKPV.js} +1 -1
- package/out/renderer/assets/{razor-BfXW9cDc.js → razor-11ECS4oH.js} +1 -1
- package/out/renderer/assets/{tsMode-BGTjG8Ow.js → tsMode-D-7JexQ_.js} +1 -1
- package/out/renderer/assets/{typescript-422MU_YO.js → typescript-Cvna1mak.js} +1 -1
- package/out/renderer/assets/{xml-B6EKhHiy.js → xml-JsEaImjA.js} +1 -1
- package/out/renderer/assets/{yaml-LkO_eGYb.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 +40 -34
- package/src/main/index.ts +95 -13
- package/src/main/orchestrator.ts +160 -55
- package/src/main/timeTracker.ts +223 -0
- package/src/main/tui.ts +25 -5
- package/src/preload/index.ts +23 -15
- package/src/renderer/App.tsx +197 -43
- package/src/renderer/components/SettingsDialog.tsx +38 -1
- package/src/renderer/components/TerminalPanel.tsx +109 -59
- package/src/renderer/styles.css +132 -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,45 +806,46 @@ 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();
|
|
812
|
+
activeTabId = null;
|
|
595
813
|
currentAgent = null;
|
|
596
814
|
currentCwd = null;
|
|
597
815
|
settings = {
|
|
598
816
|
activeProfile: "production",
|
|
599
817
|
profiles: { ...DEFAULT_PROFILES }
|
|
600
818
|
};
|
|
601
|
-
// Terminal stream batching
|
|
602
|
-
termStreamBuffer = "";
|
|
603
|
-
termStreamTimer = null;
|
|
604
819
|
constructor(settingsDir, events) {
|
|
605
820
|
this.settingsDir = settingsDir;
|
|
606
821
|
this.events = events;
|
|
607
822
|
this.workerWs = new WorkerWsClient({
|
|
608
823
|
onStatusChange: (status) => {
|
|
609
|
-
|
|
824
|
+
log3(`[worker-ws] Status: ${status}`);
|
|
610
825
|
events.onWorkerStatus(status);
|
|
611
826
|
},
|
|
612
827
|
onMessage: (message) => {
|
|
613
|
-
|
|
828
|
+
log3(`[worker-ws] Incoming message: ${message.id} (${message.type})`);
|
|
614
829
|
events.onWorkerMessage(message);
|
|
615
830
|
this.workerWs.sendAck(message.id);
|
|
616
831
|
if (message.type === "prompt" || message.type === "task_dispatch") {
|
|
617
|
-
|
|
618
|
-
|
|
832
|
+
const activeTab = this.activeTabId ? this.tabs.get(this.activeTabId) : null;
|
|
833
|
+
if (activeTab) {
|
|
834
|
+
activeTab.ptyManager.write(message.content + "\r");
|
|
619
835
|
this.bridge.feedInput(message.content);
|
|
620
836
|
}
|
|
621
837
|
}
|
|
622
838
|
},
|
|
623
839
|
onRegistered: (data) => {
|
|
624
|
-
|
|
840
|
+
log3(`[worker-ws] Registered: worker_id=${data.worker_id}, folder_id=${data.folder_id}, status=${data.status}`);
|
|
625
841
|
events.onWorkerRegistered(data);
|
|
626
842
|
if (!data.folder_id) {
|
|
627
843
|
events.onWorkerStatus("no_project");
|
|
628
844
|
}
|
|
629
845
|
},
|
|
630
846
|
onTerminalInput: (data) => {
|
|
631
|
-
this.
|
|
847
|
+
const activeTab = this.activeTabId ? this.tabs.get(this.activeTabId) : null;
|
|
848
|
+
activeTab?.ptyManager.write(data);
|
|
632
849
|
}
|
|
633
850
|
});
|
|
634
851
|
this.bridge.setWsClient(this.workerWs);
|
|
@@ -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) {
|
|
@@ -782,33 +1005,55 @@ var Orchestrator = class {
|
|
|
782
1005
|
this.saveSettings();
|
|
783
1006
|
return { ok: true };
|
|
784
1007
|
}
|
|
785
|
-
// ─── PTY & Agent
|
|
786
|
-
async spawnAgent(agent, cwd) {
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1008
|
+
// ─── PTY & Agent (multi-tab) ─────────────────────
|
|
1009
|
+
async spawnAgent(tabId, agent, cwd, opts) {
|
|
1010
|
+
const existing = this.tabs.get(tabId);
|
|
1011
|
+
if (existing) {
|
|
1012
|
+
if (existing.termStreamTimer) clearTimeout(existing.termStreamTimer);
|
|
1013
|
+
existing.ptyManager.kill();
|
|
1014
|
+
this.tabs.delete(tabId);
|
|
790
1015
|
}
|
|
791
1016
|
this.currentAgent = agent;
|
|
792
1017
|
const prevCwd = this.currentCwd;
|
|
793
1018
|
this.currentCwd = cwd;
|
|
1019
|
+
this.activeTabId = tabId;
|
|
794
1020
|
if (prevCwd !== cwd) {
|
|
795
1021
|
this.events.onCwdChanged();
|
|
796
1022
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
this.
|
|
1023
|
+
const ptyManager = new PtyManager(agent, cwd);
|
|
1024
|
+
const tab = { ptyManager, agent, cwd, termStreamBuffer: "", termStreamTimer: null };
|
|
1025
|
+
this.tabs.set(tabId, tab);
|
|
1026
|
+
ptyManager.onData((data) => {
|
|
1027
|
+
this.events.onPtyData(tabId, data);
|
|
1028
|
+
this.timeTracker.recordActivity(tabId);
|
|
1029
|
+
if (tabId === this.activeTabId) {
|
|
1030
|
+
this.bridge.feedOutput(data);
|
|
1031
|
+
this.streamTerminalData(tabId, data);
|
|
1032
|
+
}
|
|
802
1033
|
});
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
this.
|
|
806
|
-
this.
|
|
807
|
-
|
|
808
|
-
this.
|
|
1034
|
+
ptyManager.onExit(async (exitCode) => {
|
|
1035
|
+
this.events.onPtyExit(tabId, exitCode);
|
|
1036
|
+
await this.timeTracker.endSession(tabId);
|
|
1037
|
+
if (tabId === this.activeTabId) {
|
|
1038
|
+
this.bridge.endSession();
|
|
1039
|
+
if (this.currentAgent && isCodingAgent(this.currentAgent)) {
|
|
1040
|
+
this.workerWs.disconnect();
|
|
1041
|
+
}
|
|
809
1042
|
}
|
|
1043
|
+
const t = this.tabs.get(tabId);
|
|
1044
|
+
if (t?.termStreamTimer) clearTimeout(t.termStreamTimer);
|
|
810
1045
|
});
|
|
811
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
|
+
}
|
|
812
1057
|
if (isCodingAgent(agent)) {
|
|
813
1058
|
this.connectWorkerWs(agent, cwd);
|
|
814
1059
|
} else {
|
|
@@ -816,21 +1061,66 @@ var Orchestrator = class {
|
|
|
816
1061
|
this.checkProjectStatus(cwd);
|
|
817
1062
|
}
|
|
818
1063
|
}
|
|
819
|
-
writePty(data) {
|
|
820
|
-
this.ptyManager
|
|
821
|
-
this.
|
|
1064
|
+
writePty(tabId, data) {
|
|
1065
|
+
this.tabs.get(tabId)?.ptyManager.write(data);
|
|
1066
|
+
if (tabId === this.activeTabId) {
|
|
1067
|
+
this.bridge.feedInput(data);
|
|
1068
|
+
}
|
|
822
1069
|
}
|
|
823
|
-
resizePty(cols, rows) {
|
|
824
|
-
this.ptyManager
|
|
825
|
-
this.
|
|
826
|
-
|
|
1070
|
+
resizePty(tabId, cols, rows) {
|
|
1071
|
+
this.tabs.get(tabId)?.ptyManager.resize(cols, rows);
|
|
1072
|
+
if (tabId === this.activeTabId) {
|
|
1073
|
+
this.bridge.resize(cols, rows);
|
|
1074
|
+
this.workerWs.sendTerminalResize(cols, rows);
|
|
1075
|
+
}
|
|
827
1076
|
}
|
|
828
|
-
async
|
|
829
|
-
this.
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1077
|
+
async killTab(tabId) {
|
|
1078
|
+
const tab = this.tabs.get(tabId);
|
|
1079
|
+
if (!tab) return;
|
|
1080
|
+
if (tab.termStreamTimer) clearTimeout(tab.termStreamTimer);
|
|
1081
|
+
await this.timeTracker.endSession(tabId);
|
|
1082
|
+
tab.ptyManager.kill();
|
|
1083
|
+
this.tabs.delete(tabId);
|
|
1084
|
+
if (tabId === this.activeTabId) {
|
|
1085
|
+
this.bridge.endSession();
|
|
1086
|
+
if (isCodingAgent(tab.agent)) {
|
|
1087
|
+
this.workerWs.disconnect();
|
|
1088
|
+
}
|
|
1089
|
+
const remaining = [...this.tabs.keys()];
|
|
1090
|
+
this.activeTabId = remaining.length > 0 ? remaining[remaining.length - 1] : null;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
setActiveTab(tabId) {
|
|
1094
|
+
this.activeTabId = tabId;
|
|
1095
|
+
const tab = this.tabs.get(tabId);
|
|
1096
|
+
if (tab) {
|
|
1097
|
+
this.currentAgent = tab.agent;
|
|
1098
|
+
this.currentCwd = tab.cwd;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
getTabIds() {
|
|
1102
|
+
return [...this.tabs.keys()];
|
|
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);
|
|
834
1124
|
}
|
|
835
1125
|
}
|
|
836
1126
|
// ─── Worker WebSocket ───────────────────────────
|
|
@@ -838,11 +1128,11 @@ var Orchestrator = class {
|
|
|
838
1128
|
const profile = this.getActiveProfile();
|
|
839
1129
|
const apiKey = profile.apiKey || process.env.CTLSURF_API_KEY;
|
|
840
1130
|
if (!apiKey) {
|
|
841
|
-
|
|
1131
|
+
log3("[worker-ws] No API key, skipping WS connect");
|
|
842
1132
|
return;
|
|
843
1133
|
}
|
|
844
1134
|
this.workerWs.connect({
|
|
845
|
-
machine:
|
|
1135
|
+
machine: os3.hostname(),
|
|
846
1136
|
cwd,
|
|
847
1137
|
agent: agent.name
|
|
848
1138
|
});
|
|
@@ -861,44 +1151,46 @@ var Orchestrator = class {
|
|
|
861
1151
|
this.events.onWorkerStatus("no_project");
|
|
862
1152
|
}
|
|
863
1153
|
}
|
|
864
|
-
streamTerminalData(data) {
|
|
865
|
-
this.
|
|
866
|
-
if (!
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1154
|
+
streamTerminalData(tabId, data) {
|
|
1155
|
+
const tab = this.tabs.get(tabId);
|
|
1156
|
+
if (!tab) return;
|
|
1157
|
+
tab.termStreamBuffer += data;
|
|
1158
|
+
if (!tab.termStreamTimer) {
|
|
1159
|
+
tab.termStreamTimer = setTimeout(() => {
|
|
1160
|
+
if (tab.termStreamBuffer) {
|
|
1161
|
+
this.workerWs.sendTerminalData(tab.termStreamBuffer);
|
|
1162
|
+
tab.termStreamBuffer = "";
|
|
871
1163
|
}
|
|
872
|
-
|
|
1164
|
+
tab.termStreamTimer = null;
|
|
873
1165
|
}, TERM_STREAM_INTERVAL_MS);
|
|
874
1166
|
}
|
|
875
1167
|
}
|
|
876
1168
|
// ─── Shutdown ───────────────────────────────────
|
|
877
1169
|
async shutdown() {
|
|
878
1170
|
this.bridge.endSession();
|
|
879
|
-
this.
|
|
880
|
-
this.
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
clearTimeout(this.termStreamTimer);
|
|
884
|
-
this.termStreamTimer = null;
|
|
1171
|
+
await this.timeTracker.endAll();
|
|
1172
|
+
for (const [, tab] of this.tabs) {
|
|
1173
|
+
if (tab.termStreamTimer) clearTimeout(tab.termStreamTimer);
|
|
1174
|
+
tab.ptyManager.kill();
|
|
885
1175
|
}
|
|
1176
|
+
this.tabs.clear();
|
|
1177
|
+
this.workerWs.disconnect();
|
|
886
1178
|
}
|
|
887
1179
|
};
|
|
888
1180
|
|
|
889
1181
|
// src/main/settingsDir.ts
|
|
890
1182
|
import path2 from "path";
|
|
891
|
-
import
|
|
1183
|
+
import os4 from "os";
|
|
892
1184
|
function getSettingsDir(useElectron) {
|
|
893
1185
|
if (useElectron) {
|
|
894
1186
|
const { app } = require_electron();
|
|
895
1187
|
return app.getPath("userData");
|
|
896
1188
|
}
|
|
897
1189
|
if (process.platform === "darwin") {
|
|
898
|
-
return path2.join(
|
|
1190
|
+
return path2.join(os4.homedir(), "Library", "Application Support", "ctlsurf-worker");
|
|
899
1191
|
}
|
|
900
1192
|
return path2.join(
|
|
901
|
-
process.env.XDG_CONFIG_HOME || path2.join(
|
|
1193
|
+
process.env.XDG_CONFIG_HOME || path2.join(os4.homedir(), ".config"),
|
|
902
1194
|
"ctlsurf-worker"
|
|
903
1195
|
);
|
|
904
1196
|
}
|
|
@@ -1005,11 +1297,12 @@ var Tui = class {
|
|
|
1005
1297
|
* Show an interactive agent picker modal.
|
|
1006
1298
|
* Uses alternate screen just for the picker, then exits back to normal.
|
|
1007
1299
|
*/
|
|
1008
|
-
showAgentPicker(agents) {
|
|
1300
|
+
showAgentPicker(agents, options) {
|
|
1009
1301
|
return new Promise((resolve) => {
|
|
1010
1302
|
let selected = 0;
|
|
1303
|
+
let trackTime = options.initialTrackTime;
|
|
1011
1304
|
const modalWidth = 44;
|
|
1012
|
-
const modalHeight = agents.length + 4;
|
|
1305
|
+
const modalHeight = agents.length + 4 + 2;
|
|
1013
1306
|
const startCol = Math.max(1, Math.floor((this.cols - modalWidth) / 2));
|
|
1014
1307
|
const startRow = Math.max(1, Math.floor((this.rows - modalHeight) / 2));
|
|
1015
1308
|
this.write(`${CSI}?1049h`);
|
|
@@ -1043,9 +1336,19 @@ var Tui = class {
|
|
|
1043
1336
|
const pad = " ".repeat(Math.max(0, modalWidth - 2 - contentLen));
|
|
1044
1337
|
this.write(`${CSI}${row};${startCol}H${bg}${FG_DIM}\u2502${RESET}${bg}${content}${pad}${RESET}${BG_MODAL}${FG_DIM}\u2502${RESET}`);
|
|
1045
1338
|
}
|
|
1046
|
-
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;
|
|
1047
1350
|
this.write(`${CSI}${botRow};${startCol}H${BG_MODAL}${FG_DIM}${botBorder}${RESET}`);
|
|
1048
|
-
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";
|
|
1049
1352
|
const hintCol = Math.max(1, Math.floor((this.cols - hint.length) / 2));
|
|
1050
1353
|
this.write(`${CSI}${botRow + 2};${hintCol}H${FG_DIM}${hint}${RESET}`);
|
|
1051
1354
|
};
|
|
@@ -1062,9 +1365,12 @@ var Tui = class {
|
|
|
1062
1365
|
} else if (key === "\x1B[B" || key === "j") {
|
|
1063
1366
|
selected = (selected + 1) % agents.length;
|
|
1064
1367
|
drawModal();
|
|
1368
|
+
} else if (key === "t" || key === "T" || key === " ") {
|
|
1369
|
+
trackTime = !trackTime;
|
|
1370
|
+
drawModal();
|
|
1065
1371
|
} else if (key === "\r" || key === "\n") {
|
|
1066
1372
|
cleanup();
|
|
1067
|
-
resolve(selected);
|
|
1373
|
+
resolve({ agentIdx: selected, trackTime });
|
|
1068
1374
|
} else if (key === "q" || key === "\x1B" || key === "") {
|
|
1069
1375
|
cleanup();
|
|
1070
1376
|
this.write(`${CSI}?25h`);
|
|
@@ -1188,31 +1494,11 @@ async function main() {
|
|
|
1188
1494
|
const settingsDir = getSettingsDir(false);
|
|
1189
1495
|
const tui = new Tui();
|
|
1190
1496
|
const agents = getBuiltinAgents();
|
|
1191
|
-
let agent;
|
|
1192
|
-
if (args.agent) {
|
|
1193
|
-
const found = agents.find((a) => a.id === args.agent);
|
|
1194
|
-
agent = found || {
|
|
1195
|
-
id: args.agent,
|
|
1196
|
-
name: args.agent,
|
|
1197
|
-
command: args.agent,
|
|
1198
|
-
args: [],
|
|
1199
|
-
description: `Custom agent: ${args.agent}`
|
|
1200
|
-
};
|
|
1201
|
-
} else {
|
|
1202
|
-
const selectedIdx = await tui.showAgentPicker(agents);
|
|
1203
|
-
agent = agents[selectedIdx];
|
|
1204
|
-
}
|
|
1205
|
-
tui.update({
|
|
1206
|
-
agentName: agent.name,
|
|
1207
|
-
cwd: args.cwd,
|
|
1208
|
-
mode: "terminal"
|
|
1209
|
-
});
|
|
1210
|
-
tui.init();
|
|
1211
1497
|
const orchestrator = new Orchestrator(settingsDir, {
|
|
1212
|
-
onPtyData: (data) => {
|
|
1498
|
+
onPtyData: (_tabId, data) => {
|
|
1213
1499
|
tui.writePtyData(data);
|
|
1214
1500
|
},
|
|
1215
|
-
onPtyExit: (code) => {
|
|
1501
|
+
onPtyExit: (_tabId, code) => {
|
|
1216
1502
|
tui.destroy();
|
|
1217
1503
|
console.log(`Agent exited with code ${code}`);
|
|
1218
1504
|
orchestrator.shutdown().then(() => process.exit(code));
|
|
@@ -1233,12 +1519,36 @@ async function main() {
|
|
|
1233
1519
|
if (args.profile) orchestrator.switchProfile(args.profile);
|
|
1234
1520
|
if (args.apiKey) orchestrator.overrideApiKey(args.apiKey);
|
|
1235
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();
|
|
1545
|
+
const HEADLESS_TAB = "headless";
|
|
1236
1546
|
const ptySize = tui.getPtySize();
|
|
1237
|
-
await orchestrator.spawnAgent(agent, args.cwd);
|
|
1238
|
-
orchestrator.resizePty(ptySize.cols, ptySize.rows);
|
|
1547
|
+
await orchestrator.spawnAgent(HEADLESS_TAB, agent, args.cwd, { trackTime: trackTimeOverride });
|
|
1548
|
+
orchestrator.resizePty(HEADLESS_TAB, ptySize.cols, ptySize.rows);
|
|
1239
1549
|
if (isCodingAgent(agent)) {
|
|
1240
1550
|
setTimeout(() => {
|
|
1241
|
-
orchestrator.writePty("hello\r");
|
|
1551
|
+
orchestrator.writePty(HEADLESS_TAB, "hello\r");
|
|
1242
1552
|
}, 1e3);
|
|
1243
1553
|
}
|
|
1244
1554
|
const SCROLL_UP_RE = /\x1b\[<64;\d+;\d+M/;
|
|
@@ -1255,7 +1565,7 @@ async function main() {
|
|
|
1255
1565
|
if (SCROLL_UP_RE.test(str) || SCROLL_DOWN_RE.test(str)) {
|
|
1256
1566
|
return;
|
|
1257
1567
|
}
|
|
1258
|
-
orchestrator.writePty(str);
|
|
1568
|
+
orchestrator.writePty(HEADLESS_TAB, str);
|
|
1259
1569
|
});
|
|
1260
1570
|
}
|
|
1261
1571
|
process.stdout.on("resize", () => {
|
|
@@ -1263,7 +1573,7 @@ async function main() {
|
|
|
1263
1573
|
const rows = process.stdout.rows || 24;
|
|
1264
1574
|
tui.resize(cols, rows);
|
|
1265
1575
|
const size = tui.getPtySize();
|
|
1266
|
-
orchestrator.resizePty(size.cols, size.rows);
|
|
1576
|
+
orchestrator.resizePty(HEADLESS_TAB, size.cols, size.rows);
|
|
1267
1577
|
});
|
|
1268
1578
|
const shutdown = async () => {
|
|
1269
1579
|
if (process.stdin.isTTY) {
|