@phenx-inc/ctlsurf 0.3.13 → 0.3.14
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/bin/ctlsurf-worker.js +38 -22
- package/out/headless/index.mjs +247 -1
- package/out/headless/index.mjs.map +4 -4
- package/out/main/index.js +303 -46
- package/out/preload/index.js +5 -0
- package/out/renderer/assets/{cssMode-CYoo4t9f.js → cssMode-G_SDogBL.js} +3 -3
- package/out/renderer/assets/{freemarker2--UQnPZsn.js → freemarker2-BzEus0h2.js} +1 -1
- package/out/renderer/assets/{handlebars-DVDrmX0C.js → handlebars-Et995f6O.js} +1 -1
- package/out/renderer/assets/{html-D1-cXoLy.js → html-D4wgKxPD.js} +1 -1
- package/out/renderer/assets/{htmlMode-f5nBuprq.js → htmlMode-DSxpefzL.js} +3 -3
- package/out/renderer/assets/{index-65hyKM_8.css → index-AQ346NMi.css} +386 -0
- package/out/renderer/assets/{index-D23nru43.js → index-ByJTqkiQ.js} +318 -22
- package/out/renderer/assets/{javascript-CcarFzBL.js → javascript-CzLoo8aq.js} +2 -2
- package/out/renderer/assets/{jsonMode-BvF-xK9U.js → jsonMode-BrwPy7fY.js} +3 -3
- package/out/renderer/assets/{liquid-CHLtUKl2.js → liquid-BsfPf6YG.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-B9aNeatS.js → lspLanguageFeatures-CxLZ421s.js} +1 -1
- package/out/renderer/assets/{mdx-HGDrkifZ.js → mdx-CPvHIsAR.js} +1 -1
- package/out/renderer/assets/{python-B_dPzjJ6.js → python-Dr7dCUjG.js} +1 -1
- package/out/renderer/assets/{razor-CHheM4ot.js → razor-a7zjD7Y3.js} +1 -1
- package/out/renderer/assets/{tsMode-CdC3i1gG.js → tsMode-B7KLV2X6.js} +1 -1
- package/out/renderer/assets/{typescript-BX6guVRK.js → typescript-Cjuzf37q.js} +1 -1
- package/out/renderer/assets/{xml-CpS-pOPE.js → xml-Yz9xINtk.js} +1 -1
- package/out/renderer/assets/{yaml-Du0AjOHW.js → yaml-DtKnp5J0.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/src/main/ctlsurfApi.ts +11 -0
- package/src/main/index.ts +20 -0
- package/src/main/orchestrator.ts +37 -0
- package/src/main/ticketStore.ts +252 -0
- package/src/preload/index.ts +10 -0
- package/src/renderer/App.tsx +21 -0
- package/src/renderer/components/TicketPanel.tsx +308 -0
- package/src/renderer/styles.css +386 -0
package/out/main/index.js
CHANGED
|
@@ -230,6 +230,14 @@ class CtlsurfApi {
|
|
|
230
230
|
async updateRow(blockId, rowId, data) {
|
|
231
231
|
return this.request("PUT", `/datastore/${blockId}/rows/${rowId}`, { data });
|
|
232
232
|
}
|
|
233
|
+
async queryRows(blockId, opts) {
|
|
234
|
+
const params = new URLSearchParams();
|
|
235
|
+
if (opts?.orderBy) params.set("order_by", opts.orderBy);
|
|
236
|
+
if (opts?.order) params.set("order", opts.order);
|
|
237
|
+
params.set("limit", String(opts?.limit ?? 200));
|
|
238
|
+
const qs = params.toString();
|
|
239
|
+
return this.request("GET", `/datastore/${blockId}/rows${qs ? `?${qs}` : ""}`);
|
|
240
|
+
}
|
|
233
241
|
async getDatastoreSchema(blockId) {
|
|
234
242
|
return this.request("GET", `/datastore/${blockId}/schema`);
|
|
235
243
|
}
|
|
@@ -9902,7 +9910,7 @@ function requireWebsocketServer() {
|
|
|
9902
9910
|
return websocketServer;
|
|
9903
9911
|
}
|
|
9904
9912
|
requireWebsocketServer();
|
|
9905
|
-
function log$
|
|
9913
|
+
function log$3(...args) {
|
|
9906
9914
|
try {
|
|
9907
9915
|
console.log(...args);
|
|
9908
9916
|
} catch {
|
|
@@ -9999,7 +10007,7 @@ class WorkerWsClient {
|
|
|
9999
10007
|
}
|
|
10000
10008
|
doConnect() {
|
|
10001
10009
|
if (!this.apiKey || !this.registration) {
|
|
10002
|
-
log$
|
|
10010
|
+
log$3("[worker-ws] No API key or registration, skipping connect");
|
|
10003
10011
|
return;
|
|
10004
10012
|
}
|
|
10005
10013
|
this.clearTimers();
|
|
@@ -10022,22 +10030,22 @@ class WorkerWsClient {
|
|
|
10022
10030
|
doConnectNow() {
|
|
10023
10031
|
if (!this.apiKey || !this.registration) return;
|
|
10024
10032
|
if (!this.shouldReconnect) {
|
|
10025
|
-
log$
|
|
10033
|
+
log$3("[worker-ws] shouldReconnect is false, aborting connect");
|
|
10026
10034
|
return;
|
|
10027
10035
|
}
|
|
10028
10036
|
this.setStatus("connecting");
|
|
10029
10037
|
const wsBase = this.baseUrl.replace(/^http/, "ws");
|
|
10030
10038
|
const url = `${wsBase}/api/ws/worker?token=${encodeURIComponent(this.apiKey)}`;
|
|
10031
|
-
log$
|
|
10039
|
+
log$3(`[worker-ws] Connecting to ${url.replace(/token=.*/, "token=***")}...`);
|
|
10032
10040
|
try {
|
|
10033
10041
|
this.ws = new WS(url);
|
|
10034
10042
|
} catch (err) {
|
|
10035
|
-
log$
|
|
10043
|
+
log$3("[worker-ws] Failed to create WebSocket:", err);
|
|
10036
10044
|
this.scheduleReconnect();
|
|
10037
10045
|
return;
|
|
10038
10046
|
}
|
|
10039
10047
|
this.ws.onopen = () => {
|
|
10040
|
-
log$
|
|
10048
|
+
log$3("[worker-ws] Connected, sending register");
|
|
10041
10049
|
this.reconnectDelay = RECONNECT_DELAY_MS;
|
|
10042
10050
|
this.send({
|
|
10043
10051
|
type: "register",
|
|
@@ -10050,11 +10058,11 @@ class WorkerWsClient {
|
|
|
10050
10058
|
const data = JSON.parse(String(event.data));
|
|
10051
10059
|
this.handleMessage(data);
|
|
10052
10060
|
} catch (err) {
|
|
10053
|
-
log$
|
|
10061
|
+
log$3("[worker-ws] Failed to parse message:", err);
|
|
10054
10062
|
}
|
|
10055
10063
|
};
|
|
10056
10064
|
this.ws.onclose = (event) => {
|
|
10057
|
-
log$
|
|
10065
|
+
log$3(`[worker-ws] Disconnected: ${event.code} ${event.reason}`);
|
|
10058
10066
|
this.ws = null;
|
|
10059
10067
|
this.clearHeartbeat();
|
|
10060
10068
|
this.setStatus("disconnected");
|
|
@@ -10063,7 +10071,7 @@ class WorkerWsClient {
|
|
|
10063
10071
|
}
|
|
10064
10072
|
};
|
|
10065
10073
|
this.ws.onerror = () => {
|
|
10066
|
-
log$
|
|
10074
|
+
log$3("[worker-ws] WebSocket error");
|
|
10067
10075
|
};
|
|
10068
10076
|
}
|
|
10069
10077
|
handleMessage(data) {
|
|
@@ -10072,7 +10080,7 @@ class WorkerWsClient {
|
|
|
10072
10080
|
case "registered": {
|
|
10073
10081
|
this.workerId = data.worker_id;
|
|
10074
10082
|
const workerStatus = data.status;
|
|
10075
|
-
log$
|
|
10083
|
+
log$3(`[worker-ws] Registered as ${this.workerId}, status: ${workerStatus}`);
|
|
10076
10084
|
if (workerStatus === "pending_approval") {
|
|
10077
10085
|
this.setStatus("pending_approval");
|
|
10078
10086
|
} else {
|
|
@@ -10091,14 +10099,14 @@ class WorkerWsClient {
|
|
|
10091
10099
|
break;
|
|
10092
10100
|
}
|
|
10093
10101
|
case "approved": {
|
|
10094
|
-
log$
|
|
10102
|
+
log$3("[worker-ws] Worker approved!");
|
|
10095
10103
|
this.setStatus("connected");
|
|
10096
10104
|
break;
|
|
10097
10105
|
}
|
|
10098
10106
|
case "message": {
|
|
10099
10107
|
const msg = data.message;
|
|
10100
10108
|
if (msg) {
|
|
10101
|
-
log$
|
|
10109
|
+
log$3(`[worker-ws] Received message: ${msg.id}`);
|
|
10102
10110
|
this.events.onMessage(msg);
|
|
10103
10111
|
}
|
|
10104
10112
|
break;
|
|
@@ -10113,7 +10121,7 @@ class WorkerWsClient {
|
|
|
10113
10121
|
case "heartbeat_ack":
|
|
10114
10122
|
break;
|
|
10115
10123
|
default:
|
|
10116
|
-
log$
|
|
10124
|
+
log$3(`[worker-ws] Unknown message type: ${msgType}`);
|
|
10117
10125
|
}
|
|
10118
10126
|
}
|
|
10119
10127
|
send(data) {
|
|
@@ -10135,7 +10143,7 @@ class WorkerWsClient {
|
|
|
10135
10143
|
}
|
|
10136
10144
|
scheduleReconnect() {
|
|
10137
10145
|
if (!this.shouldReconnect) return;
|
|
10138
|
-
log$
|
|
10146
|
+
log$3(`[worker-ws] Reconnecting in ${this.reconnectDelay / 1e3}s...`);
|
|
10139
10147
|
this.reconnectTimer = setTimeout(() => {
|
|
10140
10148
|
this.doConnect();
|
|
10141
10149
|
}, this.reconnectDelay);
|
|
@@ -10149,12 +10157,12 @@ class WorkerWsClient {
|
|
|
10149
10157
|
}
|
|
10150
10158
|
}
|
|
10151
10159
|
}
|
|
10152
|
-
const DATASTORE_TITLE = "Time Tracking";
|
|
10153
|
-
const AGENT_DATASTORE_PAGE_TITLE = "Agent Datastore";
|
|
10154
|
-
const SYSTEM_KEY = "time_tracking";
|
|
10160
|
+
const DATASTORE_TITLE$1 = "Time Tracking";
|
|
10161
|
+
const AGENT_DATASTORE_PAGE_TITLE$1 = "Agent Datastore";
|
|
10162
|
+
const SYSTEM_KEY$1 = "time_tracking";
|
|
10155
10163
|
const FIRST_CHECKPOINT_DELAY_MS = 30 * 1e3;
|
|
10156
10164
|
const CHECKPOINT_INTERVAL_MS = 5 * 60 * 1e3;
|
|
10157
|
-
const COLUMNS = [
|
|
10165
|
+
const COLUMNS$1 = [
|
|
10158
10166
|
{ name: "Started", type: "text" },
|
|
10159
10167
|
{ name: "Active Time", type: "number" },
|
|
10160
10168
|
{ name: "Last Updated", type: "date" },
|
|
@@ -10178,17 +10186,17 @@ function isSameLocalDay(a, b2) {
|
|
|
10178
10186
|
const db = new Date(b2);
|
|
10179
10187
|
return da.getFullYear() === db.getFullYear() && da.getMonth() === db.getMonth() && da.getDate() === db.getDate();
|
|
10180
10188
|
}
|
|
10181
|
-
function log$
|
|
10189
|
+
function log$2(...args) {
|
|
10182
10190
|
try {
|
|
10183
10191
|
console.log("[time-tracker]", ...args);
|
|
10184
10192
|
} catch {
|
|
10185
10193
|
}
|
|
10186
10194
|
}
|
|
10187
|
-
function findPageByTitle(pages, title) {
|
|
10195
|
+
function findPageByTitle$1(pages, title) {
|
|
10188
10196
|
for (const p2 of pages) {
|
|
10189
10197
|
if (p2?.title === title) return p2;
|
|
10190
10198
|
if (p2?.children?.length) {
|
|
10191
|
-
const c = findPageByTitle(p2.children, title);
|
|
10199
|
+
const c = findPageByTitle$1(p2.children, title);
|
|
10192
10200
|
if (c) return c;
|
|
10193
10201
|
}
|
|
10194
10202
|
}
|
|
@@ -10234,7 +10242,7 @@ class TimeTracker {
|
|
|
10234
10242
|
}
|
|
10235
10243
|
}, FIRST_CHECKPOINT_DELAY_MS);
|
|
10236
10244
|
const pending = !state.blockId || !state.rowId;
|
|
10237
|
-
log$
|
|
10245
|
+
log$2(`Started tracking tab=${tabId} agent="${agentName}" cwd=${cwd}${pending ? " (pending datastore — will retry on each checkpoint)" : ""}`);
|
|
10238
10246
|
}
|
|
10239
10247
|
/** Attempts to locate (or create) the datastore + add the session row.
|
|
10240
10248
|
* Returns true once the session is resolved (blockId + rowId set).
|
|
@@ -10258,15 +10266,15 @@ class TimeTracker {
|
|
|
10258
10266
|
});
|
|
10259
10267
|
const rowId = row?.id;
|
|
10260
10268
|
if (!rowId) {
|
|
10261
|
-
log$
|
|
10269
|
+
log$2(`addRow returned no id for tab=${tabId}; will retry on next checkpoint`);
|
|
10262
10270
|
return false;
|
|
10263
10271
|
}
|
|
10264
10272
|
s.blockId = blockId;
|
|
10265
10273
|
s.rowId = rowId;
|
|
10266
|
-
log$
|
|
10274
|
+
log$2(`Resolved datastore for tab=${tabId} (cwd=${s.cwd})`);
|
|
10267
10275
|
return true;
|
|
10268
10276
|
} catch (err) {
|
|
10269
|
-
log$
|
|
10277
|
+
log$2(`tryResolve failed for tab=${tabId}: ${err?.message || err}`);
|
|
10270
10278
|
return false;
|
|
10271
10279
|
}
|
|
10272
10280
|
}
|
|
@@ -10294,12 +10302,12 @@ class TimeTracker {
|
|
|
10294
10302
|
if (!s || s.ended) return;
|
|
10295
10303
|
this.rollingOver.add(tabId);
|
|
10296
10304
|
const { cwd, agentName, idleTimeoutMin } = s;
|
|
10297
|
-
log$
|
|
10305
|
+
log$2(`Day rolled over for tab=${tabId}; ending session and starting fresh`);
|
|
10298
10306
|
try {
|
|
10299
10307
|
await this.endSession(tabId);
|
|
10300
10308
|
await this.startSession(tabId, cwd, agentName, idleTimeoutMin);
|
|
10301
10309
|
} catch (err) {
|
|
10302
|
-
log$
|
|
10310
|
+
log$2(`rollover failed: ${err?.message || err}`);
|
|
10303
10311
|
} finally {
|
|
10304
10312
|
this.rollingOver.delete(tabId);
|
|
10305
10313
|
}
|
|
@@ -10316,10 +10324,10 @@ class TimeTracker {
|
|
|
10316
10324
|
if (s.blockId && s.rowId) {
|
|
10317
10325
|
await this.writeRow(s, Date.now());
|
|
10318
10326
|
} else {
|
|
10319
|
-
log$
|
|
10327
|
+
log$2(`endSession for tab=${tabId}: never resolved datastore; ${Math.round(s.activeMs / 6e4)}min not recorded`);
|
|
10320
10328
|
}
|
|
10321
10329
|
} catch (err) {
|
|
10322
|
-
log$
|
|
10330
|
+
log$2(`endSession write failed: ${err?.message || err}`);
|
|
10323
10331
|
}
|
|
10324
10332
|
s.ended = true;
|
|
10325
10333
|
this.sessions.delete(tabId);
|
|
@@ -10337,12 +10345,12 @@ class TimeTracker {
|
|
|
10337
10345
|
try {
|
|
10338
10346
|
await this.writeRow(s, Date.now());
|
|
10339
10347
|
} catch (err) {
|
|
10340
|
-
log$
|
|
10348
|
+
log$2(`checkpoint failed: ${err?.message || err}; retrying in 2s`);
|
|
10341
10349
|
setTimeout(() => {
|
|
10342
10350
|
const live = this.sessions.get(tabId);
|
|
10343
10351
|
if (!live || live.ended || !live.blockId || !live.rowId) return;
|
|
10344
10352
|
this.writeRow(live, Date.now()).catch((err2) => {
|
|
10345
|
-
log$
|
|
10353
|
+
log$2(`checkpoint retry failed: ${err2?.message || err2}`);
|
|
10346
10354
|
});
|
|
10347
10355
|
}, 2e3);
|
|
10348
10356
|
}
|
|
@@ -10356,6 +10364,205 @@ class TimeTracker {
|
|
|
10356
10364
|
});
|
|
10357
10365
|
}
|
|
10358
10366
|
async ensureDatastore(cwd) {
|
|
10367
|
+
const cached = this.blockCache.get(cwd);
|
|
10368
|
+
if (cached) return cached;
|
|
10369
|
+
let folder = null;
|
|
10370
|
+
try {
|
|
10371
|
+
folder = await this.api.findFolderByPath(cwd);
|
|
10372
|
+
} catch {
|
|
10373
|
+
return null;
|
|
10374
|
+
}
|
|
10375
|
+
if (!folder?.id) return null;
|
|
10376
|
+
const folderDetail = await this.api.getFolder(folder.id);
|
|
10377
|
+
const agentPage = findPageByTitle$1(folderDetail?.pages || [], AGENT_DATASTORE_PAGE_TITLE$1);
|
|
10378
|
+
if (!agentPage?.id) return null;
|
|
10379
|
+
const blockId = await this.findOrAdoptBlock(agentPage.id);
|
|
10380
|
+
if (blockId) {
|
|
10381
|
+
await this.ensureColumns(blockId);
|
|
10382
|
+
this.blockCache.set(cwd, blockId);
|
|
10383
|
+
return blockId;
|
|
10384
|
+
}
|
|
10385
|
+
const columns = COLUMNS$1.map((c, i) => ({ id: `col_${i}`, name: c.name, type: c.type }));
|
|
10386
|
+
const created = await this.api.createBlock(agentPage.id, {
|
|
10387
|
+
type: "datastore",
|
|
10388
|
+
title: DATASTORE_TITLE$1,
|
|
10389
|
+
props: { columns, system_key: SYSTEM_KEY$1 }
|
|
10390
|
+
});
|
|
10391
|
+
if (created?.id) {
|
|
10392
|
+
log$2(`Created "${DATASTORE_TITLE$1}" datastore on Agent Datastore page for ${cwd}`);
|
|
10393
|
+
this.blockCache.set(cwd, created.id);
|
|
10394
|
+
return created.id;
|
|
10395
|
+
}
|
|
10396
|
+
return null;
|
|
10397
|
+
}
|
|
10398
|
+
/** Finds an existing Time Tracking block on the page. Prefers a system_key match
|
|
10399
|
+
* (which survives title renames). Falls back to title match for legacy blocks
|
|
10400
|
+
* created before system_key existed, and backfills system_key on the way out. */
|
|
10401
|
+
async findOrAdoptBlock(pageId) {
|
|
10402
|
+
const summaries = await this.api.getPageBlockSummaries(pageId) || [];
|
|
10403
|
+
const datastoreSummaries = summaries.filter((b2) => b2?.type === "datastore" && b2?.id);
|
|
10404
|
+
if (datastoreSummaries.length === 0) return null;
|
|
10405
|
+
let titleFallbackId = null;
|
|
10406
|
+
let titleFallbackProps = null;
|
|
10407
|
+
for (const s of datastoreSummaries) {
|
|
10408
|
+
try {
|
|
10409
|
+
const block = await this.api.getBlock(s.id);
|
|
10410
|
+
const props = block?.props || {};
|
|
10411
|
+
if (props.system_key === SYSTEM_KEY$1) {
|
|
10412
|
+
return s.id;
|
|
10413
|
+
}
|
|
10414
|
+
if (s.title === DATASTORE_TITLE$1 && titleFallbackId === null) {
|
|
10415
|
+
titleFallbackId = s.id;
|
|
10416
|
+
titleFallbackProps = props;
|
|
10417
|
+
}
|
|
10418
|
+
} catch (err) {
|
|
10419
|
+
log$2(`getBlock(${s.id}) failed during lookup: ${err?.message || err}`);
|
|
10420
|
+
}
|
|
10421
|
+
}
|
|
10422
|
+
if (titleFallbackId) {
|
|
10423
|
+
try {
|
|
10424
|
+
await this.api.updateBlock(titleFallbackId, {
|
|
10425
|
+
props: { ...titleFallbackProps || {}, system_key: SYSTEM_KEY$1 }
|
|
10426
|
+
});
|
|
10427
|
+
log$2(`Backfilled system_key on legacy Time Tracking block ${titleFallbackId}`);
|
|
10428
|
+
} catch (err) {
|
|
10429
|
+
log$2(`backfill system_key failed on ${titleFallbackId}: ${err?.message || err}`);
|
|
10430
|
+
}
|
|
10431
|
+
return titleFallbackId;
|
|
10432
|
+
}
|
|
10433
|
+
return null;
|
|
10434
|
+
}
|
|
10435
|
+
async ensureColumns(blockId) {
|
|
10436
|
+
try {
|
|
10437
|
+
const schema = await this.api.getDatastoreSchema(blockId);
|
|
10438
|
+
const existingCols = schema.columns || [];
|
|
10439
|
+
const existingNames = new Set(existingCols.map((c) => c.name));
|
|
10440
|
+
const missing = COLUMNS$1.filter((c) => !existingNames.has(c.name));
|
|
10441
|
+
if (missing.length === 0) return;
|
|
10442
|
+
const usedIds = new Set(existingCols.map((c) => c.id));
|
|
10443
|
+
let nextIdx = existingCols.length;
|
|
10444
|
+
const appended = missing.map((c) => {
|
|
10445
|
+
let id = `col_${nextIdx++}`;
|
|
10446
|
+
while (usedIds.has(id)) id = `col_${nextIdx++}`;
|
|
10447
|
+
usedIds.add(id);
|
|
10448
|
+
return { id, name: c.name, type: c.type };
|
|
10449
|
+
});
|
|
10450
|
+
const merged = [...existingCols, ...appended];
|
|
10451
|
+
await this.api.updateDatastoreSchema(blockId, merged);
|
|
10452
|
+
log$2(`Added ${missing.length} missing column(s) to existing Time Tracking datastore: ${missing.map((c) => c.name).join(", ")}`);
|
|
10453
|
+
} catch (err) {
|
|
10454
|
+
log$2(`ensureColumns failed: ${err?.message || err}`);
|
|
10455
|
+
}
|
|
10456
|
+
}
|
|
10457
|
+
}
|
|
10458
|
+
const DATASTORE_TITLE = "Tickets";
|
|
10459
|
+
const AGENT_DATASTORE_PAGE_TITLE = "Agent Datastore";
|
|
10460
|
+
const SYSTEM_KEY = "tickets";
|
|
10461
|
+
const STATUS_OPTIONS = [
|
|
10462
|
+
{ value: "Open", color: "blue" },
|
|
10463
|
+
{ value: "In Progress", color: "yellow" },
|
|
10464
|
+
{ value: "Blocked", color: "red" },
|
|
10465
|
+
{ value: "Done", color: "green" }
|
|
10466
|
+
];
|
|
10467
|
+
const PRIORITY_OPTIONS = [
|
|
10468
|
+
{ value: "Low", color: "gray" },
|
|
10469
|
+
{ value: "Med", color: "yellow" },
|
|
10470
|
+
{ value: "High", color: "red" }
|
|
10471
|
+
];
|
|
10472
|
+
const COLUMNS = [
|
|
10473
|
+
{ name: "Title", type: "text" },
|
|
10474
|
+
{ name: "Description", type: "text" },
|
|
10475
|
+
{ name: "Status", type: "select", options: STATUS_OPTIONS },
|
|
10476
|
+
{ name: "Priority", type: "select", options: PRIORITY_OPTIONS },
|
|
10477
|
+
{ name: "Created", type: "date" }
|
|
10478
|
+
];
|
|
10479
|
+
function log$1(...args) {
|
|
10480
|
+
try {
|
|
10481
|
+
console.log("[ticket-store]", ...args);
|
|
10482
|
+
} catch {
|
|
10483
|
+
}
|
|
10484
|
+
}
|
|
10485
|
+
function findPageByTitle(pages, title) {
|
|
10486
|
+
for (const p2 of pages) {
|
|
10487
|
+
if (p2?.title === title) return p2;
|
|
10488
|
+
if (p2?.children?.length) {
|
|
10489
|
+
const c = findPageByTitle(p2.children, title);
|
|
10490
|
+
if (c) return c;
|
|
10491
|
+
}
|
|
10492
|
+
}
|
|
10493
|
+
return null;
|
|
10494
|
+
}
|
|
10495
|
+
class TicketStore {
|
|
10496
|
+
api;
|
|
10497
|
+
blockCache = /* @__PURE__ */ new Map();
|
|
10498
|
+
constructor(api) {
|
|
10499
|
+
this.api = api;
|
|
10500
|
+
}
|
|
10501
|
+
/** Resolves (or creates) the project's Tickets datastore and appends a row. */
|
|
10502
|
+
async addTicket(cwd, input) {
|
|
10503
|
+
const title = input.title?.trim();
|
|
10504
|
+
if (!title) return { ok: false, error: "Title is required" };
|
|
10505
|
+
try {
|
|
10506
|
+
const blockId = await this.ensureDatastore(cwd, true);
|
|
10507
|
+
if (!blockId) {
|
|
10508
|
+
return { ok: false, error: "No ctlsurf project found for this folder" };
|
|
10509
|
+
}
|
|
10510
|
+
await this.api.addRow(blockId, {
|
|
10511
|
+
Title: title,
|
|
10512
|
+
Description: input.description?.trim() || "",
|
|
10513
|
+
Status: input.status || "Open",
|
|
10514
|
+
Priority: input.priority || "Med",
|
|
10515
|
+
Created: (/* @__PURE__ */ new Date()).toISOString()
|
|
10516
|
+
});
|
|
10517
|
+
log$1(`Added ticket "${title}" for ${cwd}`);
|
|
10518
|
+
return { ok: true };
|
|
10519
|
+
} catch (err) {
|
|
10520
|
+
log$1(`addTicket failed for ${cwd}: ${err?.message || err}`);
|
|
10521
|
+
return { ok: false, error: err?.message || String(err) };
|
|
10522
|
+
}
|
|
10523
|
+
}
|
|
10524
|
+
/** Updates an existing ticket row in the project's Tickets datastore. */
|
|
10525
|
+
async updateTicket(cwd, rowId, input) {
|
|
10526
|
+
const title = input.title?.trim();
|
|
10527
|
+
if (!title) return { ok: false, error: "Title is required" };
|
|
10528
|
+
try {
|
|
10529
|
+
const blockId = await this.ensureDatastore(cwd, false);
|
|
10530
|
+
if (!blockId) return { ok: false, error: "No Tickets datastore for this project" };
|
|
10531
|
+
await this.api.updateRow(blockId, rowId, {
|
|
10532
|
+
Title: title,
|
|
10533
|
+
Description: input.description?.trim() || "",
|
|
10534
|
+
Status: input.status || "Open",
|
|
10535
|
+
Priority: input.priority || "Med"
|
|
10536
|
+
});
|
|
10537
|
+
log$1(`Updated ticket ${rowId} for ${cwd}`);
|
|
10538
|
+
return { ok: true };
|
|
10539
|
+
} catch (err) {
|
|
10540
|
+
log$1(`updateTicket failed for ${cwd}: ${err?.message || err}`);
|
|
10541
|
+
return { ok: false, error: err?.message || String(err) };
|
|
10542
|
+
}
|
|
10543
|
+
}
|
|
10544
|
+
/** Lists existing tickets for the project, newest first. Does not create the
|
|
10545
|
+
* datastore — an unconfigured project simply has no tickets yet. */
|
|
10546
|
+
async listTickets(cwd) {
|
|
10547
|
+
try {
|
|
10548
|
+
const blockId = await this.ensureDatastore(cwd, false);
|
|
10549
|
+
if (!blockId) return { ok: true, tickets: [] };
|
|
10550
|
+
const res = await this.api.queryRows(blockId, { orderBy: "Created", order: "desc", limit: 200 });
|
|
10551
|
+
const tickets = (res?.rows || []).map((r) => ({
|
|
10552
|
+
id: r.id,
|
|
10553
|
+
title: String(r.data?.Title ?? ""),
|
|
10554
|
+
description: String(r.data?.Description ?? ""),
|
|
10555
|
+
status: String(r.data?.Status ?? "Open"),
|
|
10556
|
+
priority: String(r.data?.Priority ?? "Med"),
|
|
10557
|
+
created: r.data?.Created ?? r.created_at ?? null
|
|
10558
|
+
}));
|
|
10559
|
+
return { ok: true, tickets };
|
|
10560
|
+
} catch (err) {
|
|
10561
|
+
log$1(`listTickets failed for ${cwd}: ${err?.message || err}`);
|
|
10562
|
+
return { ok: false, tickets: [], error: err?.message || String(err) };
|
|
10563
|
+
}
|
|
10564
|
+
}
|
|
10565
|
+
async ensureDatastore(cwd, create) {
|
|
10359
10566
|
const cached = this.blockCache.get(cwd);
|
|
10360
10567
|
if (cached) return cached;
|
|
10361
10568
|
let folder = null;
|
|
@@ -10374,7 +10581,13 @@ class TimeTracker {
|
|
|
10374
10581
|
this.blockCache.set(cwd, blockId);
|
|
10375
10582
|
return blockId;
|
|
10376
10583
|
}
|
|
10377
|
-
|
|
10584
|
+
if (!create) return null;
|
|
10585
|
+
const columns = COLUMNS.map((c, i) => ({
|
|
10586
|
+
id: `col_${i}`,
|
|
10587
|
+
name: c.name,
|
|
10588
|
+
type: c.type,
|
|
10589
|
+
...c.options ? { options: c.options } : {}
|
|
10590
|
+
}));
|
|
10378
10591
|
const created = await this.api.createBlock(agentPage.id, {
|
|
10379
10592
|
type: "datastore",
|
|
10380
10593
|
title: DATASTORE_TITLE,
|
|
@@ -10387,9 +10600,9 @@ class TimeTracker {
|
|
|
10387
10600
|
}
|
|
10388
10601
|
return null;
|
|
10389
10602
|
}
|
|
10390
|
-
/** Finds an existing
|
|
10603
|
+
/** Finds an existing Tickets block on the page. Prefers a system_key match
|
|
10391
10604
|
* (which survives title renames). Falls back to title match for legacy blocks
|
|
10392
|
-
*
|
|
10605
|
+
* and backfills system_key on the way out. */
|
|
10393
10606
|
async findOrAdoptBlock(pageId) {
|
|
10394
10607
|
const summaries = await this.api.getPageBlockSummaries(pageId) || [];
|
|
10395
10608
|
const datastoreSummaries = summaries.filter((b2) => b2?.type === "datastore" && b2?.id);
|
|
@@ -10416,7 +10629,7 @@ class TimeTracker {
|
|
|
10416
10629
|
await this.api.updateBlock(titleFallbackId, {
|
|
10417
10630
|
props: { ...titleFallbackProps || {}, system_key: SYSTEM_KEY }
|
|
10418
10631
|
});
|
|
10419
|
-
log$1(`Backfilled system_key on legacy
|
|
10632
|
+
log$1(`Backfilled system_key on legacy Tickets block ${titleFallbackId}`);
|
|
10420
10633
|
} catch (err) {
|
|
10421
10634
|
log$1(`backfill system_key failed on ${titleFallbackId}: ${err?.message || err}`);
|
|
10422
10635
|
}
|
|
@@ -10437,11 +10650,11 @@ class TimeTracker {
|
|
|
10437
10650
|
let id = `col_${nextIdx++}`;
|
|
10438
10651
|
while (usedIds.has(id)) id = `col_${nextIdx++}`;
|
|
10439
10652
|
usedIds.add(id);
|
|
10440
|
-
return { id, name: c.name, type: c.type };
|
|
10653
|
+
return { id, name: c.name, type: c.type, ...c.options ? { options: c.options } : {} };
|
|
10441
10654
|
});
|
|
10442
10655
|
const merged = [...existingCols, ...appended];
|
|
10443
10656
|
await this.api.updateDatastoreSchema(blockId, merged);
|
|
10444
|
-
log$1(`Added ${missing.length} missing column(s) to existing
|
|
10657
|
+
log$1(`Added ${missing.length} missing column(s) to existing Tickets datastore: ${missing.map((c) => c.name).join(", ")}`);
|
|
10445
10658
|
} catch (err) {
|
|
10446
10659
|
log$1(`ensureColumns failed: ${err?.message || err}`);
|
|
10447
10660
|
}
|
|
@@ -10468,6 +10681,7 @@ class Orchestrator {
|
|
|
10468
10681
|
bridge = new ConversationBridge();
|
|
10469
10682
|
workerWs;
|
|
10470
10683
|
timeTracker = new TimeTracker(this.ctlsurfApi);
|
|
10684
|
+
ticketStore = new TicketStore(this.ctlsurfApi);
|
|
10471
10685
|
// State
|
|
10472
10686
|
tabs = /* @__PURE__ */ new Map();
|
|
10473
10687
|
activeTabId = null;
|
|
@@ -10485,11 +10699,11 @@ class Orchestrator {
|
|
|
10485
10699
|
this.events = events;
|
|
10486
10700
|
this.workerWs = new WorkerWsClient({
|
|
10487
10701
|
onStatusChange: (status) => {
|
|
10488
|
-
log$
|
|
10702
|
+
log$3(`[worker-ws] Status: ${status}`);
|
|
10489
10703
|
events.onWorkerStatus(status);
|
|
10490
10704
|
},
|
|
10491
10705
|
onMessage: (message) => {
|
|
10492
|
-
log$
|
|
10706
|
+
log$3(`[worker-ws] Incoming message: ${message.id} (${message.type})`);
|
|
10493
10707
|
events.onWorkerMessage(message);
|
|
10494
10708
|
this.workerWs.sendAck(message.id);
|
|
10495
10709
|
if (message.type === "prompt" || message.type === "task_dispatch") {
|
|
@@ -10501,7 +10715,7 @@ class Orchestrator {
|
|
|
10501
10715
|
}
|
|
10502
10716
|
},
|
|
10503
10717
|
onRegistered: (data) => {
|
|
10504
|
-
log$
|
|
10718
|
+
log$3(`[worker-ws] Registered: worker_id=${data.worker_id}, folder_id=${data.folder_id}, status=${data.status}`);
|
|
10505
10719
|
events.onWorkerRegistered(data);
|
|
10506
10720
|
if (!data.folder_id) {
|
|
10507
10721
|
events.onWorkerStatus("no_project");
|
|
@@ -10559,7 +10773,7 @@ class Orchestrator {
|
|
|
10559
10773
|
const baseUrl = profile.baseUrl || process.env.CTLSURF_BASE_URL || "https://app.ctlsurf.com";
|
|
10560
10774
|
this.ctlsurfApi.setBaseUrl(baseUrl);
|
|
10561
10775
|
this.workerWs.setBaseUrl(baseUrl);
|
|
10562
|
-
log$
|
|
10776
|
+
log$3(`[settings] Profile applied: ${profile.name} (${baseUrl})`);
|
|
10563
10777
|
}
|
|
10564
10778
|
loadSettings() {
|
|
10565
10779
|
try {
|
|
@@ -10584,7 +10798,7 @@ class Orchestrator {
|
|
|
10584
10798
|
logChat: !!raw.logChat
|
|
10585
10799
|
};
|
|
10586
10800
|
this.saveSettings();
|
|
10587
|
-
log$
|
|
10801
|
+
log$3("[settings] Migrated legacy settings to profiles");
|
|
10588
10802
|
} else {
|
|
10589
10803
|
this.settings = raw;
|
|
10590
10804
|
if (!this.settings.profiles.production) {
|
|
@@ -10611,7 +10825,7 @@ class Orchestrator {
|
|
|
10611
10825
|
fs.mkdirSync(this.settingsDir, { recursive: true });
|
|
10612
10826
|
fs.writeFileSync(settingsPath, JSON.stringify(this.settings, null, 2));
|
|
10613
10827
|
} catch (err) {
|
|
10614
|
-
log$
|
|
10828
|
+
log$3("[settings] Failed to save:", err.message);
|
|
10615
10829
|
}
|
|
10616
10830
|
}
|
|
10617
10831
|
overrideApiKey(key) {
|
|
@@ -10816,12 +11030,42 @@ class Orchestrator {
|
|
|
10816
11030
|
await this.timeTracker.endSession(this.activeTabId);
|
|
10817
11031
|
}
|
|
10818
11032
|
}
|
|
11033
|
+
// ─── Tickets (active tab) ───────────────────────
|
|
11034
|
+
/** cwd of the focused terminal tab, or null if no tab is active. */
|
|
11035
|
+
getActiveTabCwd() {
|
|
11036
|
+
if (!this.activeTabId) return null;
|
|
11037
|
+
return this.tabs.get(this.activeTabId)?.cwd ?? null;
|
|
11038
|
+
}
|
|
11039
|
+
async addTicketForActiveTab(input) {
|
|
11040
|
+
const cwd = this.getActiveTabCwd();
|
|
11041
|
+
if (!cwd) return { ok: false, error: "No active terminal tab" };
|
|
11042
|
+
if (!this.ctlsurfApi.getApiKey()) {
|
|
11043
|
+
return { ok: false, error: "ctlsurf API key not configured" };
|
|
11044
|
+
}
|
|
11045
|
+
return this.ticketStore.addTicket(cwd, input);
|
|
11046
|
+
}
|
|
11047
|
+
async updateTicketForActiveTab(rowId, input) {
|
|
11048
|
+
const cwd = this.getActiveTabCwd();
|
|
11049
|
+
if (!cwd) return { ok: false, error: "No active terminal tab" };
|
|
11050
|
+
if (!this.ctlsurfApi.getApiKey()) {
|
|
11051
|
+
return { ok: false, error: "ctlsurf API key not configured" };
|
|
11052
|
+
}
|
|
11053
|
+
return this.ticketStore.updateTicket(cwd, rowId, input);
|
|
11054
|
+
}
|
|
11055
|
+
async listTicketsForActiveTab() {
|
|
11056
|
+
const cwd = this.getActiveTabCwd();
|
|
11057
|
+
if (!cwd) return { ok: false, tickets: [], error: "No active terminal tab" };
|
|
11058
|
+
if (!this.ctlsurfApi.getApiKey()) {
|
|
11059
|
+
return { ok: false, tickets: [], error: "ctlsurf API key not configured" };
|
|
11060
|
+
}
|
|
11061
|
+
return this.ticketStore.listTickets(cwd);
|
|
11062
|
+
}
|
|
10819
11063
|
// ─── Worker WebSocket ───────────────────────────
|
|
10820
11064
|
connectWorkerWs(agent, cwd) {
|
|
10821
11065
|
const profile = this.getActiveProfile();
|
|
10822
11066
|
const apiKey = profile.apiKey || process.env.CTLSURF_API_KEY;
|
|
10823
11067
|
if (!apiKey) {
|
|
10824
|
-
log$
|
|
11068
|
+
log$3("[worker-ws] No API key, skipping WS connect");
|
|
10825
11069
|
return;
|
|
10826
11070
|
}
|
|
10827
11071
|
this.stopNoProjectPolling();
|
|
@@ -10835,7 +11079,7 @@ class Orchestrator {
|
|
|
10835
11079
|
if (this.noProjectPollTimer && this.noProjectPollCwd === cwd) return;
|
|
10836
11080
|
this.stopNoProjectPolling();
|
|
10837
11081
|
this.noProjectPollCwd = cwd;
|
|
10838
|
-
log$
|
|
11082
|
+
log$3(`[worker-ws] Polling for project folder at ${cwd}`);
|
|
10839
11083
|
this.noProjectPollTimer = setInterval(() => {
|
|
10840
11084
|
void this.checkForProjectFolder(cwd);
|
|
10841
11085
|
}, NO_PROJECT_POLL_MS);
|
|
@@ -10856,7 +11100,7 @@ class Orchestrator {
|
|
|
10856
11100
|
try {
|
|
10857
11101
|
const folder = await this.ctlsurfApi.findFolderByPath(cwd);
|
|
10858
11102
|
if (folder?.id && this.currentCwd === cwd && this.currentAgent) {
|
|
10859
|
-
log$
|
|
11103
|
+
log$3(`[worker-ws] Project folder appeared (${folder.id}); reconnecting`);
|
|
10860
11104
|
const agent = this.currentAgent;
|
|
10861
11105
|
this.stopNoProjectPolling();
|
|
10862
11106
|
this.workerWs.disconnect();
|
|
@@ -11181,6 +11425,19 @@ electron.ipcMain.handle("tracking:set", async (_event, enabled) => {
|
|
|
11181
11425
|
await orchestrator.setActiveTabTracking(enabled);
|
|
11182
11426
|
return { active: orchestrator.isActiveTabTracking() };
|
|
11183
11427
|
});
|
|
11428
|
+
electron.ipcMain.handle("tickets:project", () => {
|
|
11429
|
+
const cwd = orchestrator.getActiveTabCwd();
|
|
11430
|
+
return { cwd, name: cwd ? cwd.split("/").filter(Boolean).pop() || cwd : null };
|
|
11431
|
+
});
|
|
11432
|
+
electron.ipcMain.handle("tickets:add", async (_event, input) => {
|
|
11433
|
+
return orchestrator.addTicketForActiveTab(input);
|
|
11434
|
+
});
|
|
11435
|
+
electron.ipcMain.handle("tickets:update", async (_event, rowId, input) => {
|
|
11436
|
+
return orchestrator.updateTicketForActiveTab(rowId, input);
|
|
11437
|
+
});
|
|
11438
|
+
electron.ipcMain.handle("tickets:list", async () => {
|
|
11439
|
+
return orchestrator.listTicketsForActiveTab();
|
|
11440
|
+
});
|
|
11184
11441
|
electron.ipcMain.handle("settings:get", (_event, key) => {
|
|
11185
11442
|
const profile = orchestrator.getActiveProfile();
|
|
11186
11443
|
if (key === "ctlsurfApiKey") return profile.apiKey ? "***configured***" : null;
|
package/out/preload/index.js
CHANGED
|
@@ -45,6 +45,11 @@ const api = {
|
|
|
45
45
|
// Tracking (active tab)
|
|
46
46
|
getTracking: () => electron.ipcRenderer.invoke("tracking:get"),
|
|
47
47
|
setTracking: (enabled) => electron.ipcRenderer.invoke("tracking:set", enabled),
|
|
48
|
+
// Tickets (active tab)
|
|
49
|
+
getTicketProject: () => electron.ipcRenderer.invoke("tickets:project"),
|
|
50
|
+
addTicket: (input) => electron.ipcRenderer.invoke("tickets:add", input),
|
|
51
|
+
updateTicket: (rowId, input) => electron.ipcRenderer.invoke("tickets:update", rowId, input),
|
|
52
|
+
listTickets: () => electron.ipcRenderer.invoke("tickets:list"),
|
|
48
53
|
// Chat logging (global)
|
|
49
54
|
getLogChat: () => electron.ipcRenderer.invoke("logchat:get"),
|
|
50
55
|
setLogChat: (enabled) => electron.ipcRenderer.invoke("logchat:set", enabled),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { c as createWebWorker, l as languages } from "./index-
|
|
2
|
-
import { C as CompletionAdapter, H as HoverAdapter, D as DocumentHighlightAdapter, a as DefinitionAdapter, R as ReferenceAdapter, b as DocumentSymbolAdapter, c as RenameAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, e as DiagnosticsAdapter, S as SelectionRangeAdapter, f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider } from "./lspLanguageFeatures-
|
|
3
|
-
import { h, i, j, t, k } from "./lspLanguageFeatures-
|
|
1
|
+
import { c as createWebWorker, l as languages } from "./index-ByJTqkiQ.js";
|
|
2
|
+
import { C as CompletionAdapter, H as HoverAdapter, D as DocumentHighlightAdapter, a as DefinitionAdapter, R as ReferenceAdapter, b as DocumentSymbolAdapter, c as RenameAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, e as DiagnosticsAdapter, S as SelectionRangeAdapter, f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider } from "./lspLanguageFeatures-CxLZ421s.js";
|
|
3
|
+
import { h, i, j, t, k } from "./lspLanguageFeatures-CxLZ421s.js";
|
|
4
4
|
const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
|
|
5
5
|
class WorkerManager {
|
|
6
6
|
constructor(defaults) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { c as createWebWorker, l as languages } from "./index-
|
|
2
|
-
import { H as HoverAdapter, D as DocumentHighlightAdapter, h as DocumentLinkAdapter, F as FoldingRangeAdapter, b as DocumentSymbolAdapter, S as SelectionRangeAdapter, c as RenameAdapter, f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter } from "./lspLanguageFeatures-
|
|
3
|
-
import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-
|
|
1
|
+
import { c as createWebWorker, l as languages } from "./index-ByJTqkiQ.js";
|
|
2
|
+
import { H as HoverAdapter, D as DocumentHighlightAdapter, h as DocumentLinkAdapter, F as FoldingRangeAdapter, b as DocumentSymbolAdapter, S as SelectionRangeAdapter, c as RenameAdapter, f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter } from "./lspLanguageFeatures-CxLZ421s.js";
|
|
3
|
+
import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-CxLZ421s.js";
|
|
4
4
|
const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
|
|
5
5
|
class WorkerManager {
|
|
6
6
|
constructor(defaults) {
|