@phenx-inc/ctlsurf 0.12.1 → 0.13.4

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 (41) hide show
  1. package/LICENSE +22 -0
  2. package/out/headless/index.mjs +33 -36
  3. package/out/headless/index.mjs.map +3 -3
  4. package/out/main/index.js +47 -40
  5. package/out/preload/index.js +1 -0
  6. package/out/renderer/assets/{cssMode-D7r66wvO.js → cssMode-CAEObGNt.js} +3 -3
  7. package/out/renderer/assets/{freemarker2-jSWJIjPG.js → freemarker2-D63hnnDq.js} +1 -1
  8. package/out/renderer/assets/{handlebars-BnFyz5RH.js → handlebars-UYo_IYpi.js} +1 -1
  9. package/out/renderer/assets/{html-Bl5UFavp.js → html-2dvYDi3g.js} +1 -1
  10. package/out/renderer/assets/{htmlMode-D4VByBT6.js → htmlMode-B6fxDDPF.js} +3 -3
  11. package/out/renderer/assets/{index-Cc_TqOCL.css → index-BEekQe3-.css} +232 -8
  12. package/out/renderer/assets/{index-DxhW1emk.js → index-CMb4AqkZ.js} +786 -473
  13. package/out/renderer/assets/{javascript-CMN1OmwG.js → javascript-8RyFDDdk.js} +2 -2
  14. package/out/renderer/assets/{jsonMode-CJ5AYSPV.js → jsonMode-CqspkWCb.js} +3 -3
  15. package/out/renderer/assets/{liquid-BGMIDTEm.js → liquid-DaEnAsYY.js} +1 -1
  16. package/out/renderer/assets/{lspLanguageFeatures-DGifVYB5.js → lspLanguageFeatures-Czkp5sZb.js} +1 -1
  17. package/out/renderer/assets/{mdx-B2Z7R10Q.js → mdx-Q-qSZsFg.js} +1 -1
  18. package/out/renderer/assets/{python-LMDMSSdn.js → python-Sw0S0BJV.js} +1 -1
  19. package/out/renderer/assets/{razor-BZSrziPu.js → razor-C7zQmabz.js} +1 -1
  20. package/out/renderer/assets/{tsMode-DF9m6_3n.js → tsMode-BWdOxk6v.js} +1 -1
  21. package/out/renderer/assets/{typescript-rZeCMf0a.js → typescript-BrdIiKZf.js} +1 -1
  22. package/out/renderer/assets/{xml-fNSZ-7so.js → xml-DbKLNPOq.js} +1 -1
  23. package/out/renderer/assets/{yaml-1wxIuvzh.js → yaml-ehupJBsZ.js} +1 -1
  24. package/out/renderer/index.html +2 -2
  25. package/package.json +2 -2
  26. package/src/main/bridge.ts +3 -2
  27. package/src/main/index.ts +15 -0
  28. package/src/main/orchestrator.ts +1 -1
  29. package/src/main/pty.ts +2 -5
  30. package/src/main/ticketStore.ts +28 -21
  31. package/src/main/timeTracker.ts +4 -1
  32. package/src/main/transcripts.ts +3 -2
  33. package/src/preload/index.ts +2 -0
  34. package/src/renderer/App.tsx +15 -2
  35. package/src/renderer/components/CtlsurfPanel.tsx +169 -35
  36. package/src/renderer/components/SettingsDialog.tsx +13 -0
  37. package/src/renderer/components/SpeakControls.tsx +5 -0
  38. package/src/renderer/components/TerminalPanel.tsx +155 -48
  39. package/src/renderer/components/TicketPanel.tsx +80 -24
  40. package/src/renderer/lib/speech.ts +2 -1
  41. package/src/renderer/styles.css +232 -8
package/out/main/index.js CHANGED
@@ -122,6 +122,12 @@ function getSettingsDir(useElectron) {
122
122
  return app.getPath("userData");
123
123
  }
124
124
  }
125
+ function log$3(...args) {
126
+ try {
127
+ console.log(...args);
128
+ } catch {
129
+ }
130
+ }
125
131
  const require$1 = module$1.createRequire(require("url").pathToFileURL(__filename).href);
126
132
  const pty = require$1("node-pty");
127
133
  class PtyManager {
@@ -131,10 +137,7 @@ class PtyManager {
131
137
  constructor(agent, cwd) {
132
138
  const shell = agent.command;
133
139
  const args = agent.args || [];
134
- try {
135
- console.log(`[pty] Spawning: ${shell} ${args.join(" ")} in ${cwd}`);
136
- } catch {
137
- }
140
+ log$3(`[pty] Spawning: ${shell} ${args.join(" ")} in ${cwd}`);
138
141
  this.process = pty.spawn(shell, args, {
139
142
  name: "xterm-256color",
140
143
  cwd,
@@ -5963,7 +5966,7 @@ class ConversationBridge {
5963
5966
  this.inputBuffer = "";
5964
5967
  this.terminalCapture.reset();
5965
5968
  this.sessionActive = true;
5966
- console.log("[bridge] Session started");
5969
+ log$3("[bridge] Session started");
5967
5970
  }
5968
5971
  feedOutput(data) {
5969
5972
  if (!this.sessionActive || !this.loggingEnabled) return;
@@ -6043,7 +6046,7 @@ class ConversationBridge {
6043
6046
  this.sessionActive = false;
6044
6047
  this.inputBuffer = "";
6045
6048
  this.outputBuffer = "";
6046
- console.log("[bridge] Session ended");
6049
+ log$3("[bridge] Session ended");
6047
6050
  }
6048
6051
  }
6049
6052
  const DEFAULT_COLS = 80;
@@ -6252,7 +6255,7 @@ class TranscriptTailer {
6252
6255
  this.sinceMs = Date.now();
6253
6256
  this.files.clear();
6254
6257
  this.pollTimer = setInterval(() => this.poll(), POLL_INTERVAL_MS);
6255
- console.log(`[transcripts] Tailing ${this.agentId} transcripts for ${this.cwd}`);
6258
+ log$3(`[transcripts] Tailing ${this.agentId} transcripts for ${this.cwd}`);
6256
6259
  }
6257
6260
  stop() {
6258
6261
  if (!this.pollTimer) return;
@@ -6260,7 +6263,7 @@ class TranscriptTailer {
6260
6263
  this.pollTimer = null;
6261
6264
  this.poll();
6262
6265
  this.files.clear();
6263
- console.log("[transcripts] Stopped");
6266
+ log$3("[transcripts] Stopped");
6264
6267
  }
6265
6268
  poll() {
6266
6269
  try {
@@ -10689,12 +10692,6 @@ function requireWebsocketServer() {
10689
10692
  return websocketServer;
10690
10693
  }
10691
10694
  requireWebsocketServer();
10692
- function log$3(...args) {
10693
- try {
10694
- console.log(...args);
10695
- } catch {
10696
- }
10697
- }
10698
10695
  const WS = typeof WebSocket !== "undefined" ? WebSocket : WebSocket$1;
10699
10696
  const HEARTBEAT_INTERVAL_MS = 3e4;
10700
10697
  const RECONNECT_DELAY_MS = 5e3;
@@ -10969,10 +10966,7 @@ function isSameLocalDay(a, b2) {
10969
10966
  return da.getFullYear() === db.getFullYear() && da.getMonth() === db.getMonth() && da.getDate() === db.getDate();
10970
10967
  }
10971
10968
  function log$2(...args) {
10972
- try {
10973
- console.log("[time-tracker]", ...args);
10974
- } catch {
10975
- }
10969
+ log$3("[time-tracker]", ...args);
10976
10970
  }
10977
10971
  function findPageByTitle$1(pages, title) {
10978
10972
  for (const p2 of pages) {
@@ -11259,10 +11253,7 @@ const COLUMNS = [
11259
11253
  { name: "Created", type: "date" }
11260
11254
  ];
11261
11255
  function log$1(...args) {
11262
- try {
11263
- console.log("[ticket-store]", ...args);
11264
- } catch {
11265
- }
11256
+ log$3("[ticket-store]", ...args);
11266
11257
  }
11267
11258
  function findPageByTitle(pages, title) {
11268
11259
  for (const p2 of pages) {
@@ -11276,7 +11267,10 @@ function findPageByTitle(pages, title) {
11276
11267
  }
11277
11268
  class TicketStore {
11278
11269
  api;
11279
- blockCache = /* @__PURE__ */ new Map();
11270
+ // Per-cwd cache of the resolved Tickets datastore: the block (the datastore
11271
+ // element) and the Agent Datastore page it lives on. Both ids are handed to
11272
+ // the coding agent so it can read/update tickets via the ctlsurf MCP tools.
11273
+ dsCache = /* @__PURE__ */ new Map();
11280
11274
  constructor(api) {
11281
11275
  this.api = api;
11282
11276
  }
@@ -11285,11 +11279,11 @@ class TicketStore {
11285
11279
  const title = input.title?.trim();
11286
11280
  if (!title) return { ok: false, error: "Title is required" };
11287
11281
  try {
11288
- const blockId = await this.ensureDatastore(cwd, true);
11289
- if (!blockId) {
11282
+ const ds = await this.ensureDatastore(cwd, true);
11283
+ if (!ds) {
11290
11284
  return { ok: false, error: "No ctlsurf project found for this folder" };
11291
11285
  }
11292
- await this.api.addRow(blockId, {
11286
+ await this.api.addRow(ds.blockId, {
11293
11287
  Title: title,
11294
11288
  Description: input.description?.trim() || "",
11295
11289
  Status: input.status || "Open",
@@ -11308,9 +11302,9 @@ class TicketStore {
11308
11302
  const title = input.title?.trim();
11309
11303
  if (!title) return { ok: false, error: "Title is required" };
11310
11304
  try {
11311
- const blockId = await this.ensureDatastore(cwd, false);
11312
- if (!blockId) return { ok: false, error: "No Tickets datastore for this project" };
11313
- await this.api.updateRow(blockId, rowId, {
11305
+ const ds = await this.ensureDatastore(cwd, false);
11306
+ if (!ds) return { ok: false, error: "No Tickets datastore for this project" };
11307
+ await this.api.updateRow(ds.blockId, rowId, {
11314
11308
  Title: title,
11315
11309
  Description: input.description?.trim() || "",
11316
11310
  Status: input.status || "Open",
@@ -11327,9 +11321,9 @@ class TicketStore {
11327
11321
  * datastore — an unconfigured project simply has no tickets yet. */
11328
11322
  async listTickets(cwd) {
11329
11323
  try {
11330
- const blockId = await this.ensureDatastore(cwd, false);
11331
- if (!blockId) return { ok: true, tickets: [] };
11332
- const res = await this.api.queryRows(blockId, { orderBy: "Created", order: "desc", limit: 200 });
11324
+ const ds = await this.ensureDatastore(cwd, false);
11325
+ if (!ds) return { ok: true, tickets: [] };
11326
+ const res = await this.api.queryRows(ds.blockId, { orderBy: "Created", order: "desc", limit: 200 });
11333
11327
  const tickets = (res?.rows || []).map((r) => ({
11334
11328
  id: r.id,
11335
11329
  title: String(r.data?.Title ?? ""),
@@ -11338,14 +11332,14 @@ class TicketStore {
11338
11332
  priority: String(r.data?.Priority ?? "Med"),
11339
11333
  created: r.data?.Created ?? r.created_at ?? null
11340
11334
  }));
11341
- return { ok: true, tickets };
11335
+ return { ok: true, tickets, blockId: ds.blockId, pageId: ds.pageId };
11342
11336
  } catch (err) {
11343
11337
  log$1(`listTickets failed for ${cwd}: ${err?.message || err}`);
11344
11338
  return { ok: false, tickets: [], error: err?.message || String(err) };
11345
11339
  }
11346
11340
  }
11347
11341
  async ensureDatastore(cwd, create) {
11348
- const cached = this.blockCache.get(cwd);
11342
+ const cached = this.dsCache.get(cwd);
11349
11343
  if (cached) return cached;
11350
11344
  let folder = null;
11351
11345
  try {
@@ -11357,11 +11351,13 @@ class TicketStore {
11357
11351
  const folderDetail = await this.api.getFolder(folder.id);
11358
11352
  const agentPage = findPageByTitle(folderDetail?.pages || [], AGENT_DATASTORE_PAGE_TITLE);
11359
11353
  if (!agentPage?.id) return null;
11360
- const blockId = await this.findOrAdoptBlock(agentPage.id);
11354
+ const pageId = agentPage.id;
11355
+ const blockId = await this.findOrAdoptBlock(pageId);
11361
11356
  if (blockId) {
11362
11357
  await this.ensureColumns(blockId);
11363
- this.blockCache.set(cwd, blockId);
11364
- return blockId;
11358
+ const ds = { blockId, pageId };
11359
+ this.dsCache.set(cwd, ds);
11360
+ return ds;
11365
11361
  }
11366
11362
  if (!create) return null;
11367
11363
  const columns = COLUMNS.map((c, i) => ({
@@ -11370,15 +11366,16 @@ class TicketStore {
11370
11366
  type: c.type,
11371
11367
  ...c.options ? { options: c.options } : {}
11372
11368
  }));
11373
- const created = await this.api.createBlock(agentPage.id, {
11369
+ const created = await this.api.createBlock(pageId, {
11374
11370
  type: "datastore",
11375
11371
  title: DATASTORE_TITLE,
11376
11372
  props: { columns, system_key: SYSTEM_KEY }
11377
11373
  });
11378
11374
  if (created?.id) {
11379
11375
  log$1(`Created "${DATASTORE_TITLE}" datastore on Agent Datastore page for ${cwd}`);
11380
- this.blockCache.set(cwd, created.id);
11381
- return created.id;
11376
+ const ds = { blockId: created.id, pageId };
11377
+ this.dsCache.set(cwd, ds);
11378
+ return ds;
11382
11379
  }
11383
11380
  return null;
11384
11381
  }
@@ -12297,6 +12294,12 @@ function createWindow() {
12297
12294
  mainWindow.webContents.session.setPermissionRequestHandler((_wc, _permission, callback) => {
12298
12295
  callback(true);
12299
12296
  });
12297
+ mainWindow.webContents.on("did-attach-webview", (_e, contents) => {
12298
+ contents.setWindowOpenHandler(({ url }) => {
12299
+ setImmediate(() => contents.loadURL(url));
12300
+ return { action: "deny" };
12301
+ });
12302
+ });
12300
12303
  if (process.env.ELECTRON_RENDERER_URL) {
12301
12304
  mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
12302
12305
  } else {
@@ -12312,6 +12315,10 @@ electron.ipcMain.handle("clipboard:writeTable", (_event, html, text) => {
12312
12315
  electron.clipboard.write({ html, text });
12313
12316
  return { ok: true };
12314
12317
  });
12318
+ electron.ipcMain.handle("clipboard:writeText", (_event, text) => {
12319
+ electron.clipboard.writeText(text);
12320
+ return { ok: true };
12321
+ });
12315
12322
  electron.ipcMain.handle("pty:spawn", async (_event, tabId, agent, cwd) => {
12316
12323
  await orchestrator.spawnAgent(tabId, agent, cwd);
12317
12324
  return { ok: true };
@@ -42,6 +42,7 @@ const api = {
42
42
  browseCwd: () => electron.ipcRenderer.invoke("app:browseCwd"),
43
43
  // Clipboard — write a rich-HTML table (+ plain-text fallback) for email paste.
44
44
  copyEmailTable: (html, text) => electron.ipcRenderer.invoke("clipboard:writeTable", html, text),
45
+ copyText: (text) => electron.ipcRenderer.invoke("clipboard:writeText", text),
45
46
  getVersion: () => electron.ipcRenderer.invoke("app:getVersion"),
46
47
  getUpdateInfo: () => electron.ipcRenderer.invoke("app:getUpdateInfo"),
47
48
  onUpdateInfo: (callback) => {
@@ -1,6 +1,6 @@
1
- import { c as createWebWorker, l as languages } from "./index-DxhW1emk.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-DGifVYB5.js";
3
- import { h, i, j, t, k } from "./lspLanguageFeatures-DGifVYB5.js";
1
+ import { c as createWebWorker, l as languages } from "./index-CMb4AqkZ.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-Czkp5sZb.js";
3
+ import { h, i, j, t, k } from "./lspLanguageFeatures-Czkp5sZb.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-DxhW1emk.js";
1
+ import { l as languages } from "./index-CMb4AqkZ.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "assign",
4
4
  "flush",
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-DxhW1emk.js";
1
+ import { l as languages } from "./index-CMb4AqkZ.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "area",
4
4
  "base",
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-DxhW1emk.js";
1
+ import { l as languages } from "./index-CMb4AqkZ.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-DxhW1emk.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-DGifVYB5.js";
3
- import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-DGifVYB5.js";
1
+ import { c as createWebWorker, l as languages } from "./index-CMb4AqkZ.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-Czkp5sZb.js";
3
+ import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-Czkp5sZb.js";
4
4
  const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
5
5
  class WorkerManager {
6
6
  constructor(defaults) {
@@ -7915,13 +7915,88 @@ html, body, #root {
7915
7915
  width: 100%;
7916
7916
  height: 100%;
7917
7917
  overflow: hidden;
7918
+ display: flex;
7919
+ flex-direction: column;
7920
+ }
7921
+
7922
+ .ctlsurf-addressbar {
7923
+ flex: 0 0 auto;
7924
+ display: flex;
7925
+ align-items: center;
7926
+ gap: 4px;
7927
+ padding: 5px 6px;
7928
+ background: #1a1b26;
7929
+ border-bottom: 1px solid #2a2b3d;
7930
+ }
7931
+
7932
+ .ctlsurf-nav-btn {
7933
+ flex: 0 0 auto;
7934
+ width: 24px;
7935
+ height: 24px;
7936
+ display: flex;
7937
+ align-items: center;
7938
+ justify-content: center;
7939
+ padding: 0;
7940
+ background: transparent;
7941
+ border: none;
7942
+ border-radius: 4px;
7943
+ color: #a9b1d6;
7944
+ font-size: 16px;
7945
+ line-height: 1;
7946
+ cursor: pointer;
7947
+ }
7948
+
7949
+ .ctlsurf-nav-btn:hover:not(:disabled) {
7950
+ background: #2a2b3d;
7951
+ }
7952
+
7953
+ .ctlsurf-nav-btn:disabled {
7954
+ color: #565f89;
7955
+ opacity: 0.5;
7956
+ cursor: default;
7957
+ }
7958
+
7959
+ .ctlsurf-address-form {
7960
+ flex: 1 1 auto;
7961
+ display: flex;
7962
+ min-width: 0;
7963
+ }
7964
+
7965
+ .ctlsurf-address-input {
7966
+ width: 100%;
7967
+ height: 26px;
7968
+ padding: 0 11px;
7969
+ background: #16161e;
7970
+ border: 1px solid #2a2b3d;
7971
+ border-radius: 13px;
7972
+ color: #c0caf5;
7973
+ font-size: 12px;
7974
+ outline: none;
7975
+ }
7976
+
7977
+ .ctlsurf-address-input:focus {
7978
+ border-color: #7aa2f7;
7918
7979
  }
7919
7980
 
7920
- .ctlsurf-panel webview {
7981
+ .ctlsurf-webview-host {
7982
+ flex: 1 1 auto;
7983
+ position: relative;
7984
+ min-height: 0;
7985
+ }
7986
+
7987
+ .ctlsurf-webview-host webview {
7921
7988
  width: 100%;
7922
7989
  height: 100%;
7923
7990
  }
7924
7991
 
7992
+ .ctlsurf-webview-loading {
7993
+ width: 100%;
7994
+ height: 100%;
7995
+ display: flex;
7996
+ align-items: center;
7997
+ justify-content: center;
7998
+ }
7999
+
7925
8000
  /* Status bar */
7926
8001
  .statusbar {
7927
8002
  height: 24px;
@@ -8927,6 +9002,22 @@ html, body, #root {
8927
9002
  font-size: 16px;
8928
9003
  }
8929
9004
 
9005
+ /* About section inside the Settings dialog. */
9006
+ .settings-about {
9007
+ border-top: 1px solid #2a2b3d;
9008
+ padding-top: 16px;
9009
+ }
9010
+ .settings-about .about-name {
9011
+ color: #c0caf5;
9012
+ font-size: 14px;
9013
+ font-weight: 600;
9014
+ }
9015
+ .settings-about .about-copyright {
9016
+ color: #9aa5ce;
9017
+ font-size: 12px;
9018
+ margin: 6px 0 8px;
9019
+ }
9020
+
8930
9021
  .settings-section {
8931
9022
  margin-bottom: 20px;
8932
9023
  }
@@ -9438,24 +9529,57 @@ html, body, #root {
9438
9529
  .ticket-card {
9439
9530
  display: flex;
9440
9531
  align-items: stretch;
9441
- gap: 10px;
9442
9532
  background: #15161f;
9443
9533
  border: 1px solid #262a3f;
9444
9534
  border-radius: 10px;
9445
- padding: 10px 11px 10px 0;
9446
- cursor: pointer;
9447
- text-align: left;
9448
- font-family: inherit;
9449
9535
  overflow: hidden;
9450
- transition: border-color 0.13s ease, background 0.13s ease, transform 0.06s ease;
9536
+ transition: border-color 0.13s ease, background 0.13s ease;
9451
9537
  }
9452
9538
  .ticket-card:hover {
9453
9539
  border-color: #3d425f;
9454
9540
  background: #181a26;
9455
9541
  }
9456
- .ticket-card:active {
9542
+ /* Clickable body of the card — opens the editor. */
9543
+ .ticket-card-open {
9544
+ flex: 1;
9545
+ min-width: 0;
9546
+ display: flex;
9547
+ align-items: stretch;
9548
+ gap: 10px;
9549
+ padding: 10px 11px 10px 0;
9550
+ background: transparent;
9551
+ border: none;
9552
+ color: inherit;
9553
+ cursor: pointer;
9554
+ text-align: left;
9555
+ font-family: inherit;
9556
+ transition: transform 0.06s ease;
9557
+ }
9558
+ .ticket-card-open:active {
9457
9559
  transform: scale(0.99);
9458
9560
  }
9561
+ /* Trailing button that dispatches the ticket to the coding terminal. */
9562
+ .ticket-card-send {
9563
+ flex-shrink: 0;
9564
+ width: 36px;
9565
+ align-self: stretch;
9566
+ display: flex;
9567
+ align-items: center;
9568
+ justify-content: center;
9569
+ background: transparent;
9570
+ border: none;
9571
+ border-left: 1px solid #262a3f;
9572
+ color: #565f89;
9573
+ cursor: pointer;
9574
+ transition: background 0.13s ease, color 0.13s ease, transform 0.06s ease;
9575
+ }
9576
+ .ticket-card-send:hover {
9577
+ background: #1b2030;
9578
+ color: #7aa2f7;
9579
+ }
9580
+ .ticket-card-send:active {
9581
+ transform: scale(0.9);
9582
+ }
9459
9583
  .ticket-card-bar {
9460
9584
  width: 3px;
9461
9585
  border-radius: 3px;
@@ -9530,6 +9654,106 @@ html, body, #root {
9530
9654
  .term-context-menu button:hover {
9531
9655
  background: #2a2b3d;
9532
9656
  }
9657
+ /* Always-visible utility button (top-right) — opens the snapshot panel. */
9658
+ .term-util-btn {
9659
+ position: absolute;
9660
+ top: 6px;
9661
+ right: 14px;
9662
+ z-index: 6;
9663
+ width: 26px;
9664
+ height: 26px;
9665
+ display: inline-flex;
9666
+ align-items: center;
9667
+ justify-content: center;
9668
+ border-radius: 6px;
9669
+ background: #2a2b3d;
9670
+ border: 1px solid #414868;
9671
+ color: #c0caf5;
9672
+ font-size: 16px;
9673
+ line-height: 1;
9674
+ cursor: pointer;
9675
+ opacity: 0.55;
9676
+ transition: opacity 0.13s ease, background 0.13s ease;
9677
+ }
9678
+ .term-util-btn:hover {
9679
+ opacity: 1;
9680
+ background: #343650;
9681
+ }
9682
+
9683
+ /* Snapshot panel: a frozen, selectable copy of the screen text. */
9684
+ .term-snapshot {
9685
+ position: absolute;
9686
+ inset: 0;
9687
+ z-index: 20;
9688
+ display: flex;
9689
+ flex-direction: column;
9690
+ background: #16161e;
9691
+ border: 1px solid #414868;
9692
+ }
9693
+ .term-snapshot-head {
9694
+ display: flex;
9695
+ align-items: center;
9696
+ justify-content: space-between;
9697
+ gap: 12px;
9698
+ padding: 8px 12px;
9699
+ background: #1f2335;
9700
+ border-bottom: 1px solid #414868;
9701
+ color: #9aa5ce;
9702
+ font-size: 12px;
9703
+ }
9704
+ .term-snapshot-x {
9705
+ background: transparent;
9706
+ border: none;
9707
+ color: #9aa5ce;
9708
+ font-size: 14px;
9709
+ cursor: pointer;
9710
+ padding: 2px 6px;
9711
+ border-radius: 4px;
9712
+ }
9713
+ .term-snapshot-x:hover {
9714
+ background: #2a2b3d;
9715
+ color: #c0caf5;
9716
+ }
9717
+ .term-snapshot-text {
9718
+ flex: 1;
9719
+ min-height: 0;
9720
+ overflow: auto;
9721
+ margin: 0;
9722
+ padding: 10px 12px;
9723
+ white-space: pre;
9724
+ user-select: text;
9725
+ cursor: text;
9726
+ color: #c0caf5;
9727
+ background: #16161e;
9728
+ font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', monospace;
9729
+ font-size: 13px;
9730
+ line-height: 1.4;
9731
+ }
9732
+ .term-snapshot-text::selection {
9733
+ background: #33467c;
9734
+ }
9735
+ .term-snapshot-actions {
9736
+ display: flex;
9737
+ gap: 6px;
9738
+ padding: 8px 12px;
9739
+ background: #1f2335;
9740
+ border-top: 1px solid #414868;
9741
+ }
9742
+ .term-snapshot-actions button {
9743
+ background: #2a2b3d;
9744
+ border: 1px solid #414868;
9745
+ border-radius: 5px;
9746
+ color: #c0caf5;
9747
+ font-size: 12.5px;
9748
+ padding: 6px 12px;
9749
+ cursor: pointer;
9750
+ }
9751
+ .term-snapshot-actions button:hover {
9752
+ background: #343650;
9753
+ }
9754
+ .term-snapshot-done {
9755
+ margin-left: auto;
9756
+ }
9533
9757
  .term-toast {
9534
9758
  position: fixed;
9535
9759
  bottom: 24px;