@phenx-inc/ctlsurf 0.14.2 → 0.15.1

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 (40) hide show
  1. package/THIRD-PARTY-NOTICES.md +82 -0
  2. package/out/headless/index.mjs +91 -72
  3. package/out/headless/index.mjs.map +3 -3
  4. package/out/main/index.js +117 -72
  5. package/out/preload/index.js +6 -1
  6. package/out/renderer/assets/{cssMode-bQYoCKra.js → cssMode-B77M9HlN.js} +3 -3
  7. package/out/renderer/assets/{freemarker2-BS1ERQl_.js → freemarker2-oUT2zvyQ.js} +1 -1
  8. package/out/renderer/assets/{handlebars-B-ZEHRLr.js → handlebars-jCckoi5j.js} +1 -1
  9. package/out/renderer/assets/{html-lk9e1Lkl.js → html-kAgrAaKW.js} +1 -1
  10. package/out/renderer/assets/{htmlMode-Ds1eMYgM.js → htmlMode-DIWkIawy.js} +3 -3
  11. package/out/renderer/assets/{index-BpCORklm.css → index-3sFJEz7i.css} +787 -399
  12. package/out/renderer/assets/{index-BJNR4hRK.js → index-CN3UN7Tm.js} +431 -104
  13. package/out/renderer/assets/{javascript-D6OeziKX.js → javascript-B9GYUE-I.js} +2 -2
  14. package/out/renderer/assets/{jsonMode-t5SESE75.js → jsonMode-BzFKTZjJ.js} +3 -3
  15. package/out/renderer/assets/{liquid-B5Wjh0p_.js → liquid-Ccw-S_Y6.js} +1 -1
  16. package/out/renderer/assets/{lspLanguageFeatures-BEgiDCIW.js → lspLanguageFeatures-BE0g7ubH.js} +1 -1
  17. package/out/renderer/assets/{mdx-ByNAXh8t.js → mdx-oXeAA0qU.js} +1 -1
  18. package/out/renderer/assets/{python-CIFO0XQW.js → python-DlSX3TvA.js} +1 -1
  19. package/out/renderer/assets/{razor-LXHhGcNO.js → razor-ButO9S7r.js} +1 -1
  20. package/out/renderer/assets/{tsMode-WiO8CDaI.js → tsMode-CTCpE-67.js} +1 -1
  21. package/out/renderer/assets/{typescript-ZBQlq256.js → typescript-9o62WOnS.js} +1 -1
  22. package/out/renderer/assets/{xml-DBaLJlQU.js → xml-C4nOH2_D.js} +1 -1
  23. package/out/renderer/assets/{yaml-CId9skah.js → yaml-mXYgKoWW.js} +1 -1
  24. package/out/renderer/index.html +60 -3
  25. package/package.json +5 -4
  26. package/src/main/index.ts +54 -3
  27. package/src/main/orchestrator.ts +10 -2
  28. package/src/main/ticketStore.ts +100 -66
  29. package/src/preload/index.ts +13 -3
  30. package/src/renderer/App.tsx +4 -2
  31. package/src/renderer/components/CtlsurfPanel.tsx +1 -1
  32. package/src/renderer/components/EditorPanel.tsx +29 -1
  33. package/src/renderer/components/ObserverPanel.tsx +13 -6
  34. package/src/renderer/components/SettingsDialog.tsx +82 -0
  35. package/src/renderer/components/TerminalPanel.tsx +12 -22
  36. package/src/renderer/components/TicketPanel.tsx +33 -15
  37. package/src/renderer/index.html +58 -1
  38. package/src/renderer/lib/theme.ts +173 -0
  39. package/src/renderer/lib/themes.ts +126 -0
  40. package/src/renderer/styles.css +787 -399
@@ -0,0 +1,82 @@
1
+ # Third-party notices
2
+
3
+ ctlsurf is proprietary software (see `LICENSE`). This file records third-party
4
+ material it incorporates, and the terms that material is offered under.
5
+
6
+ ---
7
+
8
+ ## Colour schemes
9
+
10
+ ctlsurf ships several selectable colour schemes. For each, the palette values
11
+ were transcribed from the upstream project's **published palette specification**
12
+ and re-expressed as CSS custom properties in `src/renderer/styles.css` and as
13
+ terminal/editor palettes in `src/renderer/lib/themes.ts`.
14
+
15
+ No source code from any of these projects is included, copied, or derived from.
16
+ Scheme names are used descriptively, to identify which published palette a
17
+ theme reproduces. None of these projects endorses or is affiliated with ctlsurf.
18
+
19
+ ### Tokyo Night
20
+
21
+ - Upstream: <https://github.com/enkia/tokyo-night-vscode-theme> (MIT),
22
+ <https://github.com/folke/tokyonight.nvim> (Apache-2.0)
23
+ - Used by the `tokyo-night` and `tokyo-day` themes.
24
+
25
+ ```
26
+ MIT License
27
+
28
+ Copyright (c) 2019 enkia
29
+
30
+ Permission is hereby granted, free of charge, to any person obtaining a copy
31
+ of this software and associated documentation files (the "Software"), to deal
32
+ in the Software without restriction, including without limitation the rights
33
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
34
+ copies of the Software, and to permit persons to whom the Software is
35
+ furnished to do so, subject to the following conditions:
36
+
37
+ The above copyright notice and this permission notice shall be included in all
38
+ copies or substantial portions of the Software.
39
+
40
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
42
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
43
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
44
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
45
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
46
+ SOFTWARE.
47
+ ```
48
+
49
+ ### Catppuccin
50
+
51
+ - Upstream: <https://github.com/catppuccin/catppuccin> (MIT)
52
+ - Used by the `catppuccin-mocha` and `catppuccin-latte` themes.
53
+
54
+ ```
55
+ MIT License
56
+
57
+ Copyright (c) 2021 Catppuccin
58
+
59
+ Permission is hereby granted, free of charge, to any person obtaining a copy
60
+ of this software and associated documentation files (the "Software"), to deal
61
+ in the Software without restriction, including without limitation the rights
62
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
63
+ copies of the Software, and to permit persons to whom the Software is
64
+ furnished to do so, subject to the following conditions:
65
+
66
+ The above copyright notice and this permission notice shall be included in all
67
+ copies or substantial portions of the Software.
68
+
69
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
70
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
71
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
72
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
73
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
74
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
75
+ SOFTWARE.
76
+ ```
77
+
78
+ ---
79
+
80
+ *The licence texts above reflect the upstream projects' stated terms as of
81
+ 2026-07-25. Copyright holders and years are reproduced as published upstream;
82
+ re-verify before a release if the upstream terms have changed.*
@@ -17314,7 +17314,7 @@ var require_package = __commonJS({
17314
17314
  "package.json"(exports, module) {
17315
17315
  module.exports = {
17316
17316
  name: "@phenx-inc/ctlsurf",
17317
- version: "0.14.2",
17317
+ version: "0.15.1",
17318
17318
  description: "Agent-agnostic terminal and desktop app for ctlsurf \u2014 run Claude Code, Codex, or any coding agent with live session logging and remote control",
17319
17319
  main: "out/main/index.js",
17320
17320
  bin: {
@@ -17329,7 +17329,8 @@ var require_package = __commonJS({
17329
17329
  "src/",
17330
17330
  "package.json",
17331
17331
  "electron-vite.config.ts",
17332
- "tsconfig*.json"
17332
+ "tsconfig*.json",
17333
+ "THIRD-PARTY-NOTICES.md"
17333
17334
  ],
17334
17335
  scripts: {
17335
17336
  dev: "electron-vite dev",
@@ -19104,21 +19105,19 @@ var COLUMNS2 = [
19104
19105
  function log3(...args) {
19105
19106
  log("[ticket-store]", ...args);
19106
19107
  }
19107
- function findPageByTitle2(pages, title) {
19108
+ function findPagesByTitle(pages, title) {
19109
+ const found = [];
19108
19110
  for (const p of pages) {
19109
- if (p?.title === title) return p;
19110
- if (p?.children?.length) {
19111
- const c = findPageByTitle2(p.children, title);
19112
- if (c) return c;
19113
- }
19111
+ if (p?.title === title) found.push(p);
19112
+ if (p?.children?.length) found.push(...findPagesByTitle(p.children, title));
19114
19113
  }
19115
- return null;
19114
+ return found;
19116
19115
  }
19117
19116
  var TicketStore = class {
19118
19117
  api;
19119
- // Per-cwd cache of the resolved Tickets datastore: the block (the datastore
19120
- // element) and the Agent Datastore page it lives on. Both ids are handed to
19121
- // the coding agent so it can read/update tickets via the ctlsurf MCP tools.
19118
+ // Per-cwd cache of every resolved Tickets datastore. `primary` is where new
19119
+ // tickets are written and what gets handed to the coding agent; `all` covers
19120
+ // duplicate Agent Datastore pages so listing misses nothing.
19122
19121
  dsCache = /* @__PURE__ */ new Map();
19123
19122
  constructor(api) {
19124
19123
  this.api = api;
@@ -19128,11 +19127,11 @@ var TicketStore = class {
19128
19127
  const title = input.title?.trim();
19129
19128
  if (!title) return { ok: false, error: "Title is required" };
19130
19129
  try {
19131
- const ds = await this.ensureDatastore(cwd, true);
19132
- if (!ds) {
19130
+ const stores = await this.resolveStores(cwd, true);
19131
+ if (!stores) {
19133
19132
  return { ok: false, error: "No ctlsurf project found for this folder" };
19134
19133
  }
19135
- await this.api.addRow(ds.blockId, {
19134
+ await this.api.addRow(stores.primary.blockId, {
19136
19135
  Title: title,
19137
19136
  Description: input.description?.trim() || "",
19138
19137
  Status: input.status || "Open",
@@ -19146,14 +19145,15 @@ var TicketStore = class {
19146
19145
  return { ok: false, error: err?.message || String(err) };
19147
19146
  }
19148
19147
  }
19149
- /** Updates an existing ticket row in the project's Tickets datastore. */
19150
- async updateTicket(cwd, rowId, input) {
19148
+ /** Updates an existing ticket row. `blockId` names the store the row lives
19149
+ * in (from listTickets); without it the primary store is assumed. */
19150
+ async updateTicket(cwd, rowId, input, blockId) {
19151
19151
  const title = input.title?.trim();
19152
19152
  if (!title) return { ok: false, error: "Title is required" };
19153
19153
  try {
19154
- const ds = await this.ensureDatastore(cwd, false);
19155
- if (!ds) return { ok: false, error: "No Tickets datastore for this project" };
19156
- await this.api.updateRow(ds.blockId, rowId, {
19154
+ const stores = await this.resolveStores(cwd, false);
19155
+ if (!stores) return { ok: false, error: "No Tickets datastore for this project" };
19156
+ await this.api.updateRow(blockId || stores.primary.blockId, rowId, {
19157
19157
  Title: title,
19158
19158
  Description: input.description?.trim() || "",
19159
19159
  Status: input.status || "Open",
@@ -19166,28 +19166,39 @@ var TicketStore = class {
19166
19166
  return { ok: false, error: err?.message || String(err) };
19167
19167
  }
19168
19168
  }
19169
- /** Lists existing tickets for the project, newest first. Does not create the
19170
- * datastore — an unconfigured project simply has no tickets yet. */
19169
+ /** Lists existing tickets across every Tickets store in the project's
19170
+ * folder, newest first. Does not create the datastore — an unconfigured
19171
+ * project simply has no tickets yet. */
19171
19172
  async listTickets(cwd) {
19172
19173
  try {
19173
- const ds = await this.ensureDatastore(cwd, false);
19174
- if (!ds) return { ok: true, tickets: [] };
19175
- const res = await this.api.queryRows(ds.blockId, { orderBy: "Created", order: "desc", limit: 200 });
19176
- const tickets = (res?.rows || []).map((r) => ({
19177
- id: r.id,
19178
- title: String(r.data?.Title ?? ""),
19179
- description: String(r.data?.Description ?? ""),
19180
- status: String(r.data?.Status ?? "Open"),
19181
- priority: String(r.data?.Priority ?? "Med"),
19182
- created: r.data?.Created ?? r.created_at ?? null
19183
- }));
19184
- return { ok: true, tickets, blockId: ds.blockId, pageId: ds.pageId };
19174
+ const stores = await this.resolveStores(cwd, false);
19175
+ if (!stores) return { ok: true, tickets: [] };
19176
+ const tickets = [];
19177
+ for (const ds of stores.all) {
19178
+ const res = await this.api.queryRows(ds.blockId, { orderBy: "Created", order: "desc", limit: 200 });
19179
+ for (const r of res?.rows || []) {
19180
+ tickets.push({
19181
+ id: r.id,
19182
+ title: String(r.data?.Title ?? ""),
19183
+ description: String(r.data?.Description ?? ""),
19184
+ status: String(r.data?.Status ?? "Open"),
19185
+ priority: String(r.data?.Priority ?? "Med"),
19186
+ created: r.data?.Created ?? r.created_at ?? null,
19187
+ blockId: ds.blockId,
19188
+ pageId: ds.pageId
19189
+ });
19190
+ }
19191
+ }
19192
+ if (stores.all.length > 1) {
19193
+ tickets.sort((a, b) => (b.created || "").localeCompare(a.created || ""));
19194
+ }
19195
+ return { ok: true, tickets, blockId: stores.primary.blockId, pageId: stores.primary.pageId };
19185
19196
  } catch (err) {
19186
19197
  log3(`listTickets failed for ${cwd}: ${err?.message || err}`);
19187
19198
  return { ok: false, tickets: [], error: err?.message || String(err) };
19188
19199
  }
19189
19200
  }
19190
- async ensureDatastore(cwd, create) {
19201
+ async resolveStores(cwd, create) {
19191
19202
  const cached = this.dsCache.get(cwd);
19192
19203
  if (cached) return cached;
19193
19204
  let folder = null;
@@ -19198,17 +19209,27 @@ var TicketStore = class {
19198
19209
  }
19199
19210
  if (!folder?.id) return null;
19200
19211
  const folderDetail = await this.api.getFolder(folder.id);
19201
- const agentPage = findPageByTitle2(folderDetail?.pages || [], AGENT_DATASTORE_PAGE_TITLE2);
19202
- if (!agentPage?.id) return null;
19203
- const pageId = agentPage.id;
19204
- const blockId = await this.findOrAdoptBlock(pageId);
19205
- if (blockId) {
19206
- await this.ensureColumns(blockId);
19207
- const ds = { blockId, pageId };
19208
- this.dsCache.set(cwd, ds);
19209
- return ds;
19212
+ const agentPages = findPagesByTitle(folderDetail?.pages || [], AGENT_DATASTORE_PAGE_TITLE2);
19213
+ if (agentPages.length === 0) return null;
19214
+ const rootPath = folder.root_path || cwd;
19215
+ agentPages.sort((a, b) => Number(b?.root_path === rootPath) - Number(a?.root_path === rootPath));
19216
+ if (agentPages.length > 1) {
19217
+ log3(`Found ${agentPages.length} "Agent Datastore" pages for ${cwd}; aggregating tickets across all of them`);
19218
+ }
19219
+ const all = [];
19220
+ for (const page of agentPages) {
19221
+ for (const blockId of await this.findTicketBlocks(page.id)) {
19222
+ all.push({ blockId, pageId: page.id });
19223
+ }
19224
+ }
19225
+ if (all.length > 0) {
19226
+ for (const ds of all) await this.ensureColumns(ds.blockId);
19227
+ const stores = { primary: all[0], all };
19228
+ this.dsCache.set(cwd, stores);
19229
+ return stores;
19210
19230
  }
19211
19231
  if (!create) return null;
19232
+ const pageId = agentPages[0].id;
19212
19233
  const columns = COLUMNS2.map((c, i) => ({
19213
19234
  id: `col_${i}`,
19214
19235
  name: c.name,
@@ -19223,47 +19244,45 @@ var TicketStore = class {
19223
19244
  if (created?.id) {
19224
19245
  log3(`Created "${DATASTORE_TITLE2}" datastore on Agent Datastore page for ${cwd}`);
19225
19246
  const ds = { blockId: created.id, pageId };
19226
- this.dsCache.set(cwd, ds);
19227
- return ds;
19247
+ const stores = { primary: ds, all: [ds] };
19248
+ this.dsCache.set(cwd, stores);
19249
+ return stores;
19228
19250
  }
19229
19251
  return null;
19230
19252
  }
19231
- /** Finds an existing Tickets block on the page. Prefers a system_key match
19232
- * (which survives title renames). Falls back to title match for legacy blocks
19233
- * and backfills system_key on the way out. */
19234
- async findOrAdoptBlock(pageId) {
19253
+ /** Finds every Tickets block on the page: system_key matches first, then
19254
+ * title matches for legacy blocks. When no system_key block exists, the
19255
+ * first legacy block is adopted (system_key backfilled so it survives a
19256
+ * later rename); extra title matches are still listed but left unstamped so
19257
+ * the primary can't flip between sessions. */
19258
+ async findTicketBlocks(pageId) {
19235
19259
  const summaries = await this.api.getPageBlockSummaries(pageId) || [];
19236
19260
  const datastoreSummaries = summaries.filter((b) => b?.type === "datastore" && b?.id);
19237
- if (datastoreSummaries.length === 0) return null;
19238
- let titleFallbackId = null;
19239
- let titleFallbackProps = null;
19261
+ if (datastoreSummaries.length === 0) return [];
19262
+ const keyed = [];
19263
+ const legacy = [];
19240
19264
  for (const s of datastoreSummaries) {
19241
19265
  try {
19242
19266
  const block = await this.api.getBlock(s.id);
19243
19267
  const props = block?.props || {};
19244
- if (props.system_key === SYSTEM_KEY2) {
19245
- return s.id;
19246
- }
19247
- if (s.title === DATASTORE_TITLE2 && titleFallbackId === null) {
19248
- titleFallbackId = s.id;
19249
- titleFallbackProps = props;
19250
- }
19268
+ if (props.system_key === SYSTEM_KEY2) keyed.push(s.id);
19269
+ else if (s.title === DATASTORE_TITLE2) legacy.push({ id: s.id, props });
19251
19270
  } catch (err) {
19252
19271
  log3(`getBlock(${s.id}) failed during lookup: ${err?.message || err}`);
19253
19272
  }
19254
19273
  }
19255
- if (titleFallbackId) {
19274
+ if (keyed.length === 0 && legacy.length > 0) {
19275
+ const adopt = legacy[0];
19256
19276
  try {
19257
- await this.api.updateBlock(titleFallbackId, {
19258
- props: { ...titleFallbackProps || {}, system_key: SYSTEM_KEY2 }
19277
+ await this.api.updateBlock(adopt.id, {
19278
+ props: { ...adopt.props, system_key: SYSTEM_KEY2 }
19259
19279
  });
19260
- log3(`Backfilled system_key on legacy Tickets block ${titleFallbackId}`);
19280
+ log3(`Backfilled system_key on legacy Tickets block ${adopt.id}`);
19261
19281
  } catch (err) {
19262
- log3(`backfill system_key failed on ${titleFallbackId}: ${err?.message || err}`);
19282
+ log3(`backfill system_key failed on ${adopt.id}: ${err?.message || err}`);
19263
19283
  }
19264
- return titleFallbackId;
19265
19284
  }
19266
- return null;
19285
+ return [...keyed, ...legacy.map((l) => l.id)];
19267
19286
  }
19268
19287
  async ensureColumns(blockId) {
19269
19288
  try {
@@ -20084,11 +20103,11 @@ var COLUMNS3 = [
20084
20103
  function log4(...args) {
20085
20104
  log("[observer-store]", ...args);
20086
20105
  }
20087
- function findPageByTitle3(pages, title) {
20106
+ function findPageByTitle2(pages, title) {
20088
20107
  for (const p of pages) {
20089
20108
  if (p?.title === title) return p;
20090
20109
  if (p?.children?.length) {
20091
- const c = findPageByTitle3(p.children, title);
20110
+ const c = findPageByTitle2(p.children, title);
20092
20111
  if (c) return c;
20093
20112
  }
20094
20113
  }
@@ -20157,7 +20176,7 @@ var ObserverStore = class {
20157
20176
  }
20158
20177
  if (!folder?.id) return null;
20159
20178
  const folderDetail = await this.api.getFolder(folder.id);
20160
- const agentPage = findPageByTitle3(folderDetail?.pages || [], AGENT_DATASTORE_PAGE_TITLE3);
20179
+ const agentPage = findPageByTitle2(folderDetail?.pages || [], AGENT_DATASTORE_PAGE_TITLE3);
20161
20180
  if (!agentPage?.id) return null;
20162
20181
  const summaries = await this.api.getPageBlockSummaries(agentPage.id) || [];
20163
20182
  for (const s of summaries.filter((b) => b?.type === spec.type && b?.id)) {
@@ -21254,13 +21273,13 @@ var Orchestrator = class {
21254
21273
  }
21255
21274
  return this.ticketStore.addTicket(cwd, input);
21256
21275
  }
21257
- async updateTicketForActiveTab(rowId, input) {
21276
+ async updateTicketForActiveTab(rowId, input, blockId) {
21258
21277
  const cwd = this.getActiveTabProjectPath();
21259
21278
  if (!cwd) return { ok: false, error: "No active terminal tab" };
21260
21279
  if (!this.ctlsurfApi.getApiKey()) {
21261
21280
  return { ok: false, error: "ctlsurf API key not configured" };
21262
21281
  }
21263
- return this.ticketStore.updateTicket(cwd, rowId, input);
21282
+ return this.ticketStore.updateTicket(cwd, rowId, input, blockId);
21264
21283
  }
21265
21284
  async listTicketsForActiveTab() {
21266
21285
  const cwd = this.getActiveTabProjectPath();