@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
|
@@ -7504,6 +7504,117 @@ html, body, #root {
|
|
|
7504
7504
|
overflow: hidden;
|
|
7505
7505
|
}
|
|
7506
7506
|
|
|
7507
|
+
/* Terminal tabs wrapper */
|
|
7508
|
+
.terminal-tabs-wrapper {
|
|
7509
|
+
display: flex;
|
|
7510
|
+
flex-direction: column;
|
|
7511
|
+
width: 100%;
|
|
7512
|
+
height: 100%;
|
|
7513
|
+
overflow: hidden;
|
|
7514
|
+
}
|
|
7515
|
+
|
|
7516
|
+
.terminal-tab-bar {
|
|
7517
|
+
display: flex;
|
|
7518
|
+
align-items: center;
|
|
7519
|
+
gap: 1px;
|
|
7520
|
+
padding: 2px 4px;
|
|
7521
|
+
background: #16161e;
|
|
7522
|
+
border-bottom: 1px solid #292e42;
|
|
7523
|
+
flex-shrink: 0;
|
|
7524
|
+
overflow-x: auto;
|
|
7525
|
+
min-height: 28px;
|
|
7526
|
+
}
|
|
7527
|
+
|
|
7528
|
+
.terminal-tab {
|
|
7529
|
+
display: flex;
|
|
7530
|
+
align-items: center;
|
|
7531
|
+
gap: 4px;
|
|
7532
|
+
padding: 3px 8px;
|
|
7533
|
+
font-size: 11px;
|
|
7534
|
+
color: #565f89;
|
|
7535
|
+
background: transparent;
|
|
7536
|
+
border-radius: 4px;
|
|
7537
|
+
cursor: pointer;
|
|
7538
|
+
white-space: nowrap;
|
|
7539
|
+
user-select: none;
|
|
7540
|
+
}
|
|
7541
|
+
|
|
7542
|
+
.terminal-tab:hover {
|
|
7543
|
+
background: #1f2335;
|
|
7544
|
+
color: #a9b1d6;
|
|
7545
|
+
}
|
|
7546
|
+
|
|
7547
|
+
.terminal-tab.active {
|
|
7548
|
+
background: #1a1b26;
|
|
7549
|
+
color: #c0caf5;
|
|
7550
|
+
}
|
|
7551
|
+
|
|
7552
|
+
.terminal-tab-label {
|
|
7553
|
+
display: flex;
|
|
7554
|
+
align-items: center;
|
|
7555
|
+
gap: 4px;
|
|
7556
|
+
}
|
|
7557
|
+
|
|
7558
|
+
.terminal-tab-dot {
|
|
7559
|
+
display: inline-block;
|
|
7560
|
+
width: 6px;
|
|
7561
|
+
height: 6px;
|
|
7562
|
+
border-radius: 50%;
|
|
7563
|
+
}
|
|
7564
|
+
|
|
7565
|
+
.terminal-tab-dot.active {
|
|
7566
|
+
background: #9ece6a;
|
|
7567
|
+
}
|
|
7568
|
+
|
|
7569
|
+
.terminal-tab-dot.exited {
|
|
7570
|
+
background: #565f89;
|
|
7571
|
+
}
|
|
7572
|
+
|
|
7573
|
+
.terminal-tab-close {
|
|
7574
|
+
background: none;
|
|
7575
|
+
border: none;
|
|
7576
|
+
color: inherit;
|
|
7577
|
+
font-size: 14px;
|
|
7578
|
+
line-height: 1;
|
|
7579
|
+
cursor: pointer;
|
|
7580
|
+
padding: 0 2px;
|
|
7581
|
+
border-radius: 3px;
|
|
7582
|
+
opacity: 0.5;
|
|
7583
|
+
}
|
|
7584
|
+
|
|
7585
|
+
.terminal-tab-close:hover {
|
|
7586
|
+
opacity: 1;
|
|
7587
|
+
background: rgba(247, 118, 142, 0.2);
|
|
7588
|
+
color: #f7768e;
|
|
7589
|
+
}
|
|
7590
|
+
|
|
7591
|
+
.terminal-tab-add {
|
|
7592
|
+
background: none;
|
|
7593
|
+
border: none;
|
|
7594
|
+
color: #565f89;
|
|
7595
|
+
font-size: 16px;
|
|
7596
|
+
cursor: pointer;
|
|
7597
|
+
padding: 2px 6px;
|
|
7598
|
+
border-radius: 4px;
|
|
7599
|
+
line-height: 1;
|
|
7600
|
+
}
|
|
7601
|
+
|
|
7602
|
+
.terminal-tab-add:hover {
|
|
7603
|
+
background: #1f2335;
|
|
7604
|
+
color: #a9b1d6;
|
|
7605
|
+
}
|
|
7606
|
+
|
|
7607
|
+
.terminal-tabs-content {
|
|
7608
|
+
flex: 1;
|
|
7609
|
+
display: flex;
|
|
7610
|
+
overflow: hidden;
|
|
7611
|
+
}
|
|
7612
|
+
|
|
7613
|
+
.terminal-tab-panel {
|
|
7614
|
+
flex-direction: column;
|
|
7615
|
+
overflow: hidden;
|
|
7616
|
+
}
|
|
7617
|
+
|
|
7507
7618
|
/* Terminal container */
|
|
7508
7619
|
.terminal-container {
|
|
7509
7620
|
width: 100%;
|
|
@@ -7793,6 +7904,27 @@ html, body, #root {
|
|
|
7793
7904
|
.status-dot.idle { background: #565f89; }
|
|
7794
7905
|
.status-dot.pending { background: #e0af68; }
|
|
7795
7906
|
|
|
7907
|
+
.tracking-dot {
|
|
7908
|
+
width: 6px;
|
|
7909
|
+
height: 6px;
|
|
7910
|
+
border-radius: 50%;
|
|
7911
|
+
display: inline-block;
|
|
7912
|
+
vertical-align: middle;
|
|
7913
|
+
}
|
|
7914
|
+
.tracking-dot.on { background: #9ece6a; box-shadow: 0 0 4px #9ece6a; }
|
|
7915
|
+
.tracking-dot.off { background: #565f89; }
|
|
7916
|
+
|
|
7917
|
+
.titlebar-icon-btn {
|
|
7918
|
+
display: inline-flex;
|
|
7919
|
+
align-items: center;
|
|
7920
|
+
gap: 5px;
|
|
7921
|
+
padding: 0 8px;
|
|
7922
|
+
}
|
|
7923
|
+
.tracking-icon {
|
|
7924
|
+
font-size: 14px;
|
|
7925
|
+
line-height: 1;
|
|
7926
|
+
}
|
|
7927
|
+
|
|
7796
7928
|
/* Editor panel */
|
|
7797
7929
|
.editor-panel {
|
|
7798
7930
|
display: flex;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { conf as conf$1, language as language$1 } from "./typescript-
|
|
2
|
-
import "./index-
|
|
1
|
+
import { conf as conf$1, language as language$1 } from "./typescript-Cvna1mak.js";
|
|
2
|
+
import "./index-BnCJ1IaZ.js";
|
|
3
3
|
const conf = conf$1;
|
|
4
4
|
const language = {
|
|
5
5
|
// Set defaultToken to invalid to see what you do not tokenize yet
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { c as createWebWorker, l as languages, e as editor } from "./index-
|
|
2
|
-
import { f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter, H as HoverAdapter, b as DocumentSymbolAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, S as SelectionRangeAdapter, e as DiagnosticsAdapter } from "./lspLanguageFeatures-
|
|
3
|
-
import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-
|
|
1
|
+
import { c as createWebWorker, l as languages, e as editor } from "./index-BnCJ1IaZ.js";
|
|
2
|
+
import { f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter, H as HoverAdapter, b as DocumentSymbolAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, S as SelectionRangeAdapter, e as DiagnosticsAdapter } from "./lspLanguageFeatures-CUafmPGy.js";
|
|
3
|
+
import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-CUafmPGy.js";
|
|
4
4
|
const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
|
|
5
5
|
class WorkerManager {
|
|
6
6
|
constructor(defaults) {
|
package/out/renderer/assets/{lspLanguageFeatures-Dljhj5Gh.js → lspLanguageFeatures-CUafmPGy.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index-
|
|
1
|
+
import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index-BnCJ1IaZ.js";
|
|
2
2
|
var DocumentUri;
|
|
3
3
|
(function(DocumentUri2) {
|
|
4
4
|
function is(value) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as createWebWorker, e as editor, U as Uri, a as MarkerTag, M as MarkerSeverity, l as languages, t as typescriptDefaults, R as Range } from "./index-
|
|
1
|
+
import { c as createWebWorker, e as editor, U as Uri, a as MarkerTag, M as MarkerSeverity, l as languages, t as typescriptDefaults, R as Range } from "./index-BnCJ1IaZ.js";
|
|
2
2
|
class WorkerManager {
|
|
3
3
|
constructor(_modeId, _defaults) {
|
|
4
4
|
this._modeId = _modeId;
|
package/out/renderer/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>ctlsurf-worker</title>
|
|
7
|
-
<script type="module" crossorigin src="./assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-BnCJ1IaZ.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="./assets/index-CrTu3Z4M.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phenx-inc/ctlsurf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Agent-agnostic terminal and desktop app for ctlsurf — run Claude Code, Codex, or any coding agent with live session logging and remote control",
|
|
5
5
|
"main": "out/main/index.js",
|
|
6
6
|
"bin": {
|
package/src/main/ctlsurfApi.ts
CHANGED
|
@@ -105,6 +105,32 @@ export class CtlsurfApi {
|
|
|
105
105
|
return folder?.pages || []
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
async getFolder(folderId: string): Promise<any> {
|
|
109
|
+
return this.request('GET', `/folders/${folderId}`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Datastore ───────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
async getPageBlockSummaries(pageId: string): Promise<any[]> {
|
|
115
|
+
return this.request('GET', `/blocks/page/${pageId}/summary`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async addRow(blockId: string, data: Record<string, unknown>): Promise<any> {
|
|
119
|
+
return this.request('POST', `/datastore/${blockId}/rows`, { data })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async updateRow(blockId: string, rowId: string, data: Record<string, unknown>): Promise<any> {
|
|
123
|
+
return this.request('PUT', `/datastore/${blockId}/rows/${rowId}`, { data })
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async getDatastoreSchema(blockId: string): Promise<{ block_id: string; columns: Array<{ id: string; name: string; type: string }> }> {
|
|
127
|
+
return this.request('GET', `/datastore/${blockId}/schema`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async updateDatastoreSchema(blockId: string, columns: Array<{ id: string; name: string; type: string }>): Promise<any> {
|
|
131
|
+
return this.request('PUT', `/datastore/${blockId}/schema`, { columns })
|
|
132
|
+
}
|
|
133
|
+
|
|
108
134
|
async findFolderByGitRemote(gitRemote: string): Promise<any> {
|
|
109
135
|
// Search folders by listing all and matching git_remote
|
|
110
136
|
const folders = await this.request('GET', '/folders')
|
package/src/main/headless.ts
CHANGED
|
@@ -70,39 +70,13 @@ async function main() {
|
|
|
70
70
|
const tui = new Tui()
|
|
71
71
|
const agents = getBuiltinAgents()
|
|
72
72
|
|
|
73
|
-
// ───
|
|
74
|
-
let agent: AgentConfig
|
|
75
|
-
|
|
76
|
-
if (args.agent) {
|
|
77
|
-
const found = agents.find(a => a.id === args.agent)
|
|
78
|
-
agent = found || {
|
|
79
|
-
id: args.agent,
|
|
80
|
-
name: args.agent,
|
|
81
|
-
command: args.agent,
|
|
82
|
-
args: [],
|
|
83
|
-
description: `Custom agent: ${args.agent}`,
|
|
84
|
-
}
|
|
85
|
-
} else {
|
|
86
|
-
// Show interactive picker
|
|
87
|
-
const selectedIdx = await tui.showAgentPicker(agents)
|
|
88
|
-
agent = agents[selectedIdx]
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ─── Start TUI + agent ─────────────────────────
|
|
92
|
-
|
|
93
|
-
tui.update({
|
|
94
|
-
agentName: agent.name,
|
|
95
|
-
cwd: args.cwd,
|
|
96
|
-
mode: 'terminal',
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
tui.init()
|
|
73
|
+
// ─── Orchestrator (loaded early so picker can read profile defaults) ──
|
|
100
74
|
|
|
101
75
|
const orchestrator = new Orchestrator(settingsDir, {
|
|
102
|
-
onPtyData: (data) => {
|
|
76
|
+
onPtyData: (_tabId, data) => {
|
|
103
77
|
tui.writePtyData(data)
|
|
104
78
|
},
|
|
105
|
-
onPtyExit: (code) => {
|
|
79
|
+
onPtyExit: (_tabId, code) => {
|
|
106
80
|
tui.destroy()
|
|
107
81
|
console.log(`Agent exited with code ${code}`)
|
|
108
82
|
orchestrator.shutdown().then(() => process.exit(code))
|
|
@@ -125,15 +99,47 @@ async function main() {
|
|
|
125
99
|
if (args.apiKey) orchestrator.overrideApiKey(args.apiKey)
|
|
126
100
|
if (args.baseUrl) orchestrator.overrideBaseUrl(args.baseUrl)
|
|
127
101
|
|
|
102
|
+
// ─── Agent selection ────────────────────────────
|
|
103
|
+
|
|
104
|
+
let agent: AgentConfig
|
|
105
|
+
let trackTimeOverride: boolean | undefined
|
|
106
|
+
|
|
107
|
+
if (args.agent) {
|
|
108
|
+
const found = agents.find(a => a.id === args.agent)
|
|
109
|
+
agent = found || {
|
|
110
|
+
id: args.agent,
|
|
111
|
+
name: args.agent,
|
|
112
|
+
command: args.agent,
|
|
113
|
+
args: [],
|
|
114
|
+
description: `Custom agent: ${args.agent}`,
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
const initialTrackTime = orchestrator.getActiveProfile().trackTime !== false
|
|
118
|
+
const picked = await tui.showAgentPicker(agents, { initialTrackTime })
|
|
119
|
+
agent = agents[picked.agentIdx]
|
|
120
|
+
trackTimeOverride = picked.trackTime
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── Start TUI + agent ─────────────────────────
|
|
124
|
+
|
|
125
|
+
tui.update({
|
|
126
|
+
agentName: agent.name,
|
|
127
|
+
cwd: args.cwd,
|
|
128
|
+
mode: 'terminal',
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
tui.init()
|
|
132
|
+
|
|
128
133
|
// Spawn agent with PTY sized to fit the TUI content area
|
|
134
|
+
const HEADLESS_TAB = 'headless'
|
|
129
135
|
const ptySize = tui.getPtySize()
|
|
130
|
-
await orchestrator.spawnAgent(agent, args.cwd)
|
|
131
|
-
orchestrator.resizePty(ptySize.cols, ptySize.rows)
|
|
136
|
+
await orchestrator.spawnAgent(HEADLESS_TAB, agent, args.cwd, { trackTime: trackTimeOverride })
|
|
137
|
+
orchestrator.resizePty(HEADLESS_TAB, ptySize.cols, ptySize.rows)
|
|
132
138
|
|
|
133
139
|
// For coding agents, send an initial prompt to kick-start them
|
|
134
140
|
if (isCodingAgent(agent)) {
|
|
135
141
|
setTimeout(() => {
|
|
136
|
-
orchestrator.writePty('hello\r')
|
|
142
|
+
orchestrator.writePty(HEADLESS_TAB, 'hello\r')
|
|
137
143
|
}, 1000)
|
|
138
144
|
}
|
|
139
145
|
|
|
@@ -160,7 +166,7 @@ async function main() {
|
|
|
160
166
|
if (SCROLL_UP_RE.test(str) || SCROLL_DOWN_RE.test(str)) {
|
|
161
167
|
return
|
|
162
168
|
}
|
|
163
|
-
orchestrator.writePty(str)
|
|
169
|
+
orchestrator.writePty(HEADLESS_TAB, str)
|
|
164
170
|
})
|
|
165
171
|
}
|
|
166
172
|
|
|
@@ -170,7 +176,7 @@ async function main() {
|
|
|
170
176
|
const rows = process.stdout.rows || 24
|
|
171
177
|
tui.resize(cols, rows)
|
|
172
178
|
const size = tui.getPtySize()
|
|
173
|
-
orchestrator.resizePty(size.cols, size.rows)
|
|
179
|
+
orchestrator.resizePty(HEADLESS_TAB, size.cols, size.rows)
|
|
174
180
|
})
|
|
175
181
|
|
|
176
182
|
// Graceful shutdown
|
package/src/main/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { app, BrowserWindow, ipcMain, dialog } from 'electron'
|
|
1
|
+
import { app, BrowserWindow, ipcMain, dialog, nativeImage } from 'electron'
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import fs from 'fs'
|
|
4
4
|
|
|
@@ -19,17 +19,88 @@ import { Orchestrator } from './orchestrator'
|
|
|
19
19
|
|
|
20
20
|
let mainWindow: BrowserWindow | null = null
|
|
21
21
|
|
|
22
|
+
// ─── Project badge on dock/taskbar icon ──────────
|
|
23
|
+
|
|
24
|
+
function getProjectAbbrev(cwdPath: string): string {
|
|
25
|
+
const folderName = cwdPath.split('/').filter(Boolean).pop() || ''
|
|
26
|
+
if (!folderName) return ''
|
|
27
|
+
|
|
28
|
+
// Split on hyphens, underscores, dots, camelCase boundaries
|
|
29
|
+
const words = folderName
|
|
30
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase → separate words
|
|
31
|
+
.split(/[-_.\s]+/)
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
|
|
34
|
+
if (words.length >= 2) {
|
|
35
|
+
// Multiple words → take initials (up to 4)
|
|
36
|
+
return words.slice(0, 4).map(w => w[0]).join('').toUpperCase()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Single word: if short (<=3 chars) use as-is, otherwise first 2 chars
|
|
40
|
+
const word = words[0]
|
|
41
|
+
if (word.length <= 3) return word.toUpperCase()
|
|
42
|
+
return word.slice(0, 2).toUpperCase()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createOverlayIcon(text: string): Electron.NativeImage {
|
|
46
|
+
// Create a 32x32 overlay with text for Windows/Linux
|
|
47
|
+
const size = 32
|
|
48
|
+
const canvas = Buffer.alloc(size * size * 4, 0) // RGBA
|
|
49
|
+
|
|
50
|
+
// Fill with a semi-transparent dark background circle
|
|
51
|
+
for (let y = 0; y < size; y++) {
|
|
52
|
+
for (let x = 0; x < size; x++) {
|
|
53
|
+
const cx = x - size / 2
|
|
54
|
+
const cy = y - size / 2
|
|
55
|
+
const dist = Math.sqrt(cx * cx + cy * cy)
|
|
56
|
+
if (dist <= size / 2) {
|
|
57
|
+
const idx = (y * size + x) * 4
|
|
58
|
+
canvas[idx] = 122 // R (blue-ish)
|
|
59
|
+
canvas[idx + 1] = 162 // G
|
|
60
|
+
canvas[idx + 2] = 247 // B
|
|
61
|
+
canvas[idx + 3] = 220 // A
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return nativeImage.createFromBuffer(canvas, { width: size, height: size })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function updateProjectBadge(cwdPath: string | null): void {
|
|
70
|
+
if (!cwdPath) return
|
|
71
|
+
const abbrev = getProjectAbbrev(cwdPath)
|
|
72
|
+
|
|
73
|
+
// macOS: dock badge shows text directly
|
|
74
|
+
if (process.platform === 'darwin' && app.dock) {
|
|
75
|
+
app.dock.setBadge(abbrev)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Windows: overlay icon on taskbar
|
|
79
|
+
if (process.platform === 'win32' && mainWindow) {
|
|
80
|
+
mainWindow.setOverlayIcon(createOverlayIcon(abbrev), abbrev)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// All platforms: update window title
|
|
84
|
+
if (mainWindow) {
|
|
85
|
+
const folderName = cwdPath.split('/').filter(Boolean).pop() || 'ctlsurf-worker'
|
|
86
|
+
mainWindow.setTitle(`ctlsurf-worker — ${folderName}`)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
22
90
|
// ─── Orchestrator (shared logic) ──────────────────
|
|
23
91
|
|
|
24
92
|
const orchestrator = new Orchestrator(
|
|
25
93
|
getSettingsDir(true),
|
|
26
94
|
{
|
|
27
|
-
onPtyData: (data) => mainWindow?.webContents.send('pty:data', data),
|
|
28
|
-
onPtyExit: (code) => mainWindow?.webContents.send('pty:exit', code),
|
|
95
|
+
onPtyData: (tabId, data) => mainWindow?.webContents.send('pty:data', tabId, data),
|
|
96
|
+
onPtyExit: (tabId, code) => mainWindow?.webContents.send('pty:exit', tabId, code),
|
|
29
97
|
onWorkerStatus: (status) => mainWindow?.webContents.send('worker:status', status),
|
|
30
98
|
onWorkerMessage: (message) => mainWindow?.webContents.send('worker:message', message),
|
|
31
99
|
onWorkerRegistered: (data) => mainWindow?.webContents.send('worker:registered', data),
|
|
32
|
-
onCwdChanged: () =>
|
|
100
|
+
onCwdChanged: () => {
|
|
101
|
+
mainWindow?.webContents.send('app:cwdChanged')
|
|
102
|
+
updateProjectBadge(orchestrator.cwd)
|
|
103
|
+
},
|
|
33
104
|
}
|
|
34
105
|
)
|
|
35
106
|
|
|
@@ -65,21 +136,25 @@ function createWindow(): void {
|
|
|
65
136
|
|
|
66
137
|
// ─── IPC Handlers ──────────────────────────────────
|
|
67
138
|
|
|
68
|
-
ipcMain.handle('pty:spawn', async (_event, agent: AgentConfig, cwd: string) => {
|
|
69
|
-
await orchestrator.spawnAgent(agent, cwd)
|
|
139
|
+
ipcMain.handle('pty:spawn', async (_event, tabId: string, agent: AgentConfig, cwd: string) => {
|
|
140
|
+
await orchestrator.spawnAgent(tabId, agent, cwd)
|
|
70
141
|
return { ok: true }
|
|
71
142
|
})
|
|
72
143
|
|
|
73
|
-
ipcMain.handle('pty:write', (_event, data: string) => {
|
|
74
|
-
orchestrator.writePty(data)
|
|
144
|
+
ipcMain.handle('pty:write', (_event, tabId: string, data: string) => {
|
|
145
|
+
orchestrator.writePty(tabId, data)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
ipcMain.handle('pty:resize', (_event, tabId: string, cols: number, rows: number) => {
|
|
149
|
+
orchestrator.resizePty(tabId, cols, rows)
|
|
75
150
|
})
|
|
76
151
|
|
|
77
|
-
ipcMain.handle('pty:
|
|
78
|
-
orchestrator.
|
|
152
|
+
ipcMain.handle('pty:kill', async (_event, tabId: string) => {
|
|
153
|
+
await orchestrator.killTab(tabId)
|
|
79
154
|
})
|
|
80
155
|
|
|
81
|
-
ipcMain.handle('pty:
|
|
82
|
-
|
|
156
|
+
ipcMain.handle('pty:setActiveTab', (_event, tabId: string) => {
|
|
157
|
+
orchestrator.setActiveTab(tabId)
|
|
83
158
|
})
|
|
84
159
|
|
|
85
160
|
ipcMain.handle('agents:list', () => getBuiltinAgents())
|
|
@@ -234,6 +309,14 @@ ipcMain.handle('profiles:save', (_event, id: string, data: any) => {
|
|
|
234
309
|
ipcMain.handle('profiles:switch', (_event, id: string) => orchestrator.switchProfile(id))
|
|
235
310
|
ipcMain.handle('profiles:delete', (_event, id: string) => orchestrator.deleteProfile(id))
|
|
236
311
|
|
|
312
|
+
// ─── Tracking IPC ─────────────────────────────────
|
|
313
|
+
|
|
314
|
+
ipcMain.handle('tracking:get', () => ({ active: orchestrator.isActiveTabTracking() }))
|
|
315
|
+
ipcMain.handle('tracking:set', async (_event, enabled: boolean) => {
|
|
316
|
+
await orchestrator.setActiveTabTracking(enabled)
|
|
317
|
+
return { active: orchestrator.isActiveTabTracking() }
|
|
318
|
+
})
|
|
319
|
+
|
|
237
320
|
// ─── Legacy Settings IPC ──────────────────────────
|
|
238
321
|
|
|
239
322
|
ipcMain.handle('settings:get', (_event, key: string) => {
|
|
@@ -279,7 +362,6 @@ app.whenReady().then(() => {
|
|
|
279
362
|
if (process.platform === 'darwin' && app.dock) {
|
|
280
363
|
const iconPath = path.join(__dirname, '../../resources/icon.png')
|
|
281
364
|
try {
|
|
282
|
-
const { nativeImage } = require('electron')
|
|
283
365
|
app.dock.setIcon(nativeImage.createFromPath(iconPath))
|
|
284
366
|
} catch { /* ignore */ }
|
|
285
367
|
}
|