@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.
Files changed (33) hide show
  1. package/bin/ctlsurf-worker.js +38 -22
  2. package/out/headless/index.mjs +247 -1
  3. package/out/headless/index.mjs.map +4 -4
  4. package/out/main/index.js +303 -46
  5. package/out/preload/index.js +5 -0
  6. package/out/renderer/assets/{cssMode-CYoo4t9f.js → cssMode-G_SDogBL.js} +3 -3
  7. package/out/renderer/assets/{freemarker2--UQnPZsn.js → freemarker2-BzEus0h2.js} +1 -1
  8. package/out/renderer/assets/{handlebars-DVDrmX0C.js → handlebars-Et995f6O.js} +1 -1
  9. package/out/renderer/assets/{html-D1-cXoLy.js → html-D4wgKxPD.js} +1 -1
  10. package/out/renderer/assets/{htmlMode-f5nBuprq.js → htmlMode-DSxpefzL.js} +3 -3
  11. package/out/renderer/assets/{index-65hyKM_8.css → index-AQ346NMi.css} +386 -0
  12. package/out/renderer/assets/{index-D23nru43.js → index-ByJTqkiQ.js} +318 -22
  13. package/out/renderer/assets/{javascript-CcarFzBL.js → javascript-CzLoo8aq.js} +2 -2
  14. package/out/renderer/assets/{jsonMode-BvF-xK9U.js → jsonMode-BrwPy7fY.js} +3 -3
  15. package/out/renderer/assets/{liquid-CHLtUKl2.js → liquid-BsfPf6YG.js} +1 -1
  16. package/out/renderer/assets/{lspLanguageFeatures-B9aNeatS.js → lspLanguageFeatures-CxLZ421s.js} +1 -1
  17. package/out/renderer/assets/{mdx-HGDrkifZ.js → mdx-CPvHIsAR.js} +1 -1
  18. package/out/renderer/assets/{python-B_dPzjJ6.js → python-Dr7dCUjG.js} +1 -1
  19. package/out/renderer/assets/{razor-CHheM4ot.js → razor-a7zjD7Y3.js} +1 -1
  20. package/out/renderer/assets/{tsMode-CdC3i1gG.js → tsMode-B7KLV2X6.js} +1 -1
  21. package/out/renderer/assets/{typescript-BX6guVRK.js → typescript-Cjuzf37q.js} +1 -1
  22. package/out/renderer/assets/{xml-CpS-pOPE.js → xml-Yz9xINtk.js} +1 -1
  23. package/out/renderer/assets/{yaml-Du0AjOHW.js → yaml-DtKnp5J0.js} +1 -1
  24. package/out/renderer/index.html +2 -2
  25. package/package.json +1 -1
  26. package/src/main/ctlsurfApi.ts +11 -0
  27. package/src/main/index.ts +20 -0
  28. package/src/main/orchestrator.ts +37 -0
  29. package/src/main/ticketStore.ts +252 -0
  30. package/src/preload/index.ts +10 -0
  31. package/src/renderer/App.tsx +21 -0
  32. package/src/renderer/components/TicketPanel.tsx +308 -0
  33. 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$2(...args) {
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$2("[worker-ws] No API key or registration, skipping connect");
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$2("[worker-ws] shouldReconnect is false, aborting connect");
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$2(`[worker-ws] Connecting to ${url.replace(/token=.*/, "token=***")}...`);
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$2("[worker-ws] Failed to create WebSocket:", err);
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$2("[worker-ws] Connected, sending register");
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$2("[worker-ws] Failed to parse message:", err);
10061
+ log$3("[worker-ws] Failed to parse message:", err);
10054
10062
  }
10055
10063
  };
10056
10064
  this.ws.onclose = (event) => {
10057
- log$2(`[worker-ws] Disconnected: ${event.code} ${event.reason}`);
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$2("[worker-ws] WebSocket error");
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$2(`[worker-ws] Registered as ${this.workerId}, status: ${workerStatus}`);
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$2("[worker-ws] Worker approved!");
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$2(`[worker-ws] Received message: ${msg.id}`);
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$2(`[worker-ws] Unknown message type: ${msgType}`);
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$2(`[worker-ws] Reconnecting in ${this.reconnectDelay / 1e3}s...`);
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$1(...args) {
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$1(`Started tracking tab=${tabId} agent="${agentName}" cwd=${cwd}${pending ? " (pending datastore — will retry on each checkpoint)" : ""}`);
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$1(`addRow returned no id for tab=${tabId}; will retry on next checkpoint`);
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$1(`Resolved datastore for tab=${tabId} (cwd=${s.cwd})`);
10274
+ log$2(`Resolved datastore for tab=${tabId} (cwd=${s.cwd})`);
10267
10275
  return true;
10268
10276
  } catch (err) {
10269
- log$1(`tryResolve failed for tab=${tabId}: ${err?.message || err}`);
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$1(`Day rolled over for tab=${tabId}; ending session and starting fresh`);
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$1(`rollover failed: ${err?.message || err}`);
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$1(`endSession for tab=${tabId}: never resolved datastore; ${Math.round(s.activeMs / 6e4)}min not recorded`);
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$1(`endSession write failed: ${err?.message || err}`);
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$1(`checkpoint failed: ${err?.message || err}; retrying in 2s`);
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$1(`checkpoint retry failed: ${err2?.message || err2}`);
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
- const columns = COLUMNS.map((c, i) => ({ id: `col_${i}`, name: c.name, type: c.type }));
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 Time Tracking block on the page. Prefers a system_key match
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
- * created before system_key existed, and backfills system_key on the way out. */
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 Time Tracking block ${titleFallbackId}`);
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 Time Tracking datastore: ${missing.map((c) => c.name).join(", ")}`);
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$2(`[worker-ws] Status: ${status}`);
10702
+ log$3(`[worker-ws] Status: ${status}`);
10489
10703
  events.onWorkerStatus(status);
10490
10704
  },
10491
10705
  onMessage: (message) => {
10492
- log$2(`[worker-ws] Incoming message: ${message.id} (${message.type})`);
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$2(`[worker-ws] Registered: worker_id=${data.worker_id}, folder_id=${data.folder_id}, status=${data.status}`);
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$2(`[settings] Profile applied: ${profile.name} (${baseUrl})`);
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$2("[settings] Migrated legacy settings to profiles");
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$2("[settings] Failed to save:", err.message);
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$2("[worker-ws] No API key, skipping WS connect");
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$2(`[worker-ws] Polling for project folder at ${cwd}`);
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$2(`[worker-ws] Project folder appeared (${folder.id}); reconnecting`);
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;
@@ -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-D23nru43.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-B9aNeatS.js";
3
- import { h, i, j, t, k } from "./lspLanguageFeatures-B9aNeatS.js";
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,4 +1,4 @@
1
- import { l as languages } from "./index-D23nru43.js";
1
+ import { l as languages } from "./index-ByJTqkiQ.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "assign",
4
4
  "flush",
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-D23nru43.js";
1
+ import { l as languages } from "./index-ByJTqkiQ.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "area",
4
4
  "base",
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-D23nru43.js";
1
+ import { l as languages } from "./index-ByJTqkiQ.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "area",
4
4
  "base",
@@ -1,6 +1,6 @@
1
- import { c as createWebWorker, l as languages } from "./index-D23nru43.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-B9aNeatS.js";
3
- import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-B9aNeatS.js";
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) {