@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
@@ -1,5 +1,5 @@
1
- import { conf as conf$1, language as language$1 } from "./typescript-rZeCMf0a.js";
2
- import "./index-DxhW1emk.js";
1
+ import { conf as conf$1, language as language$1 } from "./typescript-BrdIiKZf.js";
2
+ import "./index-CMb4AqkZ.js";
3
3
  const conf = conf$1;
4
4
  const language = {
5
5
  // Set defaultToken to invalid to see what you do not tokenize yet
@@ -1,6 +1,6 @@
1
- import { c as createWebWorker, l as languages, e as editor } from "./index-DxhW1emk.js";
2
- import { f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter, H as HoverAdapter, b as DocumentSymbolAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, S as SelectionRangeAdapter, e as DiagnosticsAdapter } from "./lspLanguageFeatures-DGifVYB5.js";
3
- import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-DGifVYB5.js";
1
+ import { c as createWebWorker, l as languages, e as editor } from "./index-CMb4AqkZ.js";
2
+ import { f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter, H as HoverAdapter, b as DocumentSymbolAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, S as SelectionRangeAdapter, e as DiagnosticsAdapter } from "./lspLanguageFeatures-Czkp5sZb.js";
3
+ import { a, D, h, R, c, 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
  "area",
4
4
  "base",
@@ -1,4 +1,4 @@
1
- import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index-DxhW1emk.js";
1
+ import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index-CMb4AqkZ.js";
2
2
  var DocumentUri;
3
3
  (function(DocumentUri2) {
4
4
  function is(value) {
@@ -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 conf = {
3
3
  comments: {
4
4
  blockComment: ["{/*", "*/}"]
@@ -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 conf = {
3
3
  comments: {
4
4
  lineComment: "#",
@@ -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 { c as createWebWorker, e as editor, U as Uri, a as MarkerTag, M as MarkerSeverity, l as languages, t as typescriptDefaults, R as Range } from "./index-DxhW1emk.js";
1
+ import { c as createWebWorker, e as editor, U as Uri, a as MarkerTag, M as MarkerSeverity, l as languages, t as typescriptDefaults, R as Range } from "./index-CMb4AqkZ.js";
2
2
  class WorkerManager {
3
3
  constructor(_modeId, _defaults) {
4
4
  this._modeId = _modeId;
@@ -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 conf = {
3
3
  wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
4
4
  comments: {
@@ -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 conf = {
3
3
  comments: {
4
4
  blockComment: ["<!--", "-->"]
@@ -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 conf = {
3
3
  comments: {
4
4
  lineComment: "#"
@@ -49,8 +49,8 @@
49
49
  color: #565f89;
50
50
  }
51
51
  </style>
52
- <script type="module" crossorigin src="./assets/index-DxhW1emk.js"></script>
53
- <link rel="stylesheet" crossorigin href="./assets/index-Cc_TqOCL.css">
52
+ <script type="module" crossorigin src="./assets/index-CMb4AqkZ.js"></script>
53
+ <link rel="stylesheet" crossorigin href="./assets/index-BEekQe3-.css">
54
54
  </head>
55
55
  <body>
56
56
  <div id="boot-splash">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phenx-inc/ctlsurf",
3
- "version": "0.12.1",
3
+ "version": "0.13.4",
4
4
  "description": "Agent-agnostic terminal and desktop app for ctlsurf — run Claude Code, Codex, or any coding agent with live session logging and remote control",
5
5
  "main": "out/main/index.js",
6
6
  "bin": {
@@ -38,7 +38,7 @@
38
38
  "node-pty",
39
39
  "xterm"
40
40
  ],
41
- "license": "MIT",
41
+ "license": "SEE LICENSE IN LICENSE",
42
42
  "engines": {
43
43
  "node": ">=18"
44
44
  },
@@ -1,6 +1,7 @@
1
1
  import { WorkerWsClient } from './workerWs'
2
2
  import { Terminal as HeadlessTerminal } from '@xterm/headless/lib-headless/xterm-headless.js'
3
3
  import { SerializeAddon } from '@xterm/addon-serialize'
4
+ import { log } from './logger'
4
5
 
5
6
  /**
6
7
  * Conversation Bridge
@@ -36,7 +37,7 @@ export class ConversationBridge {
36
37
  this.inputBuffer = ''
37
38
  this.terminalCapture.reset()
38
39
  this.sessionActive = true
39
- console.log('[bridge] Session started')
40
+ log('[bridge] Session started')
40
41
  }
41
42
 
42
43
  feedOutput(data: string): void {
@@ -137,7 +138,7 @@ export class ConversationBridge {
137
138
  this.sessionActive = false
138
139
  this.inputBuffer = ''
139
140
  this.outputBuffer = ''
140
- console.log('[bridge] Session ended')
141
+ log('[bridge] Session ended')
141
142
  }
142
143
  }
143
144
 
package/src/main/index.ts CHANGED
@@ -254,6 +254,15 @@ function createWindow(): void {
254
254
  callback(true)
255
255
  })
256
256
 
257
+ // Keep links opened from inside the embedded ctlsurf webview (target=_blank,
258
+ // window.open) in the panel instead of spawning a detached popup window.
259
+ mainWindow.webContents.on('did-attach-webview', (_e, contents) => {
260
+ contents.setWindowOpenHandler(({ url }) => {
261
+ setImmediate(() => contents.loadURL(url))
262
+ return { action: 'deny' }
263
+ })
264
+ })
265
+
257
266
  if (process.env.ELECTRON_RENDERER_URL) {
258
267
  mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL)
259
268
  } else {
@@ -280,6 +289,12 @@ ipcMain.handle('clipboard:writeTable', (_event, html: string, text: string) => {
280
289
  return { ok: true }
281
290
  })
282
291
 
292
+ // Plain-text clipboard write, for the terminal right-click "Copy" item.
293
+ ipcMain.handle('clipboard:writeText', (_event, text: string) => {
294
+ clipboard.writeText(text)
295
+ return { ok: true }
296
+ })
297
+
283
298
  ipcMain.handle('pty:spawn', async (_event, tabId: string, agent: AgentConfig, cwd: string) => {
284
299
  await orchestrator.spawnAgent(tabId, agent, cwd)
285
300
  return { ok: true }
@@ -708,7 +708,7 @@ export class Orchestrator {
708
708
  return this.ticketStore.updateTicket(cwd, rowId, input)
709
709
  }
710
710
 
711
- async listTicketsForActiveTab(): Promise<{ ok: boolean; tickets: import('./ticketStore').Ticket[]; error?: string }> {
711
+ async listTicketsForActiveTab(): Promise<{ ok: boolean; tickets: import('./ticketStore').Ticket[]; blockId?: string; pageId?: string; error?: string }> {
712
712
  const cwd = this.getActiveTabProjectPath()
713
713
  if (!cwd) return { ok: false, tickets: [], error: 'No active terminal tab' }
714
714
  if (!this.ctlsurfApi.getApiKey()) {
package/src/main/pty.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createRequire } from 'module'
2
2
  import { AgentConfig } from './agents'
3
+ import { log } from './logger'
3
4
 
4
5
  // Use createRequire to load native module at runtime, bypassing bundler
5
6
  const require = createRequire(import.meta.url)
@@ -14,11 +15,7 @@ export class PtyManager {
14
15
  const shell = agent.command
15
16
  const args = agent.args || []
16
17
 
17
- try {
18
- console.log(`[pty] Spawning: ${shell} ${args.join(' ')} in ${cwd}`)
19
- } catch {
20
- // Ignore EPIPE errors when stdout is closed
21
- }
18
+ log(`[pty] Spawning: ${shell} ${args.join(' ')} in ${cwd}`)
22
19
 
23
20
  this.process = pty.spawn(shell, args, {
24
21
  name: 'xterm-256color',
@@ -1,4 +1,5 @@
1
1
  import type { CtlsurfApi } from './ctlsurfApi'
2
+ import { log as baseLog } from './logger'
2
3
 
3
4
  const DATASTORE_TITLE = 'Tickets'
4
5
  const AGENT_DATASTORE_PAGE_TITLE = 'Agent Datastore'
@@ -47,7 +48,7 @@ export interface Ticket {
47
48
  }
48
49
 
49
50
  function log(...args: unknown[]): void {
50
- try { console.log('[ticket-store]', ...args) } catch { /* EPIPE safe */ }
51
+ baseLog('[ticket-store]', ...args)
51
52
  }
52
53
 
53
54
  function findPageByTitle(pages: any[], title: string): any | null {
@@ -69,7 +70,10 @@ function findPageByTitle(pages: any[], title: string): any | null {
69
70
  */
70
71
  export class TicketStore {
71
72
  private api: CtlsurfApi
72
- private blockCache = new Map<string, string>()
73
+ // Per-cwd cache of the resolved Tickets datastore: the block (the datastore
74
+ // element) and the Agent Datastore page it lives on. Both ids are handed to
75
+ // the coding agent so it can read/update tickets via the ctlsurf MCP tools.
76
+ private dsCache = new Map<string, { blockId: string; pageId: string }>()
73
77
 
74
78
  constructor(api: CtlsurfApi) {
75
79
  this.api = api
@@ -80,11 +84,11 @@ export class TicketStore {
80
84
  const title = input.title?.trim()
81
85
  if (!title) return { ok: false, error: 'Title is required' }
82
86
  try {
83
- const blockId = await this.ensureDatastore(cwd, true)
84
- if (!blockId) {
87
+ const ds = await this.ensureDatastore(cwd, true)
88
+ if (!ds) {
85
89
  return { ok: false, error: 'No ctlsurf project found for this folder' }
86
90
  }
87
- await this.api.addRow(blockId, {
91
+ await this.api.addRow(ds.blockId, {
88
92
  Title: title,
89
93
  Description: input.description?.trim() || '',
90
94
  Status: input.status || 'Open',
@@ -104,9 +108,9 @@ export class TicketStore {
104
108
  const title = input.title?.trim()
105
109
  if (!title) return { ok: false, error: 'Title is required' }
106
110
  try {
107
- const blockId = await this.ensureDatastore(cwd, false)
108
- if (!blockId) return { ok: false, error: 'No Tickets datastore for this project' }
109
- await this.api.updateRow(blockId, rowId, {
111
+ const ds = await this.ensureDatastore(cwd, false)
112
+ if (!ds) return { ok: false, error: 'No Tickets datastore for this project' }
113
+ await this.api.updateRow(ds.blockId, rowId, {
110
114
  Title: title,
111
115
  Description: input.description?.trim() || '',
112
116
  Status: input.status || 'Open',
@@ -122,11 +126,11 @@ export class TicketStore {
122
126
 
123
127
  /** Lists existing tickets for the project, newest first. Does not create the
124
128
  * datastore — an unconfigured project simply has no tickets yet. */
125
- async listTickets(cwd: string): Promise<{ ok: boolean; tickets: Ticket[]; error?: string }> {
129
+ async listTickets(cwd: string): Promise<{ ok: boolean; tickets: Ticket[]; blockId?: string; pageId?: string; error?: string }> {
126
130
  try {
127
- const blockId = await this.ensureDatastore(cwd, false)
128
- if (!blockId) return { ok: true, tickets: [] }
129
- const res = await this.api.queryRows(blockId, { orderBy: 'Created', order: 'desc', limit: 200 })
131
+ const ds = await this.ensureDatastore(cwd, false)
132
+ if (!ds) return { ok: true, tickets: [] }
133
+ const res = await this.api.queryRows(ds.blockId, { orderBy: 'Created', order: 'desc', limit: 200 })
130
134
  const tickets: Ticket[] = (res?.rows || []).map(r => ({
131
135
  id: r.id,
132
136
  title: String(r.data?.Title ?? ''),
@@ -135,15 +139,15 @@ export class TicketStore {
135
139
  priority: String(r.data?.Priority ?? 'Med'),
136
140
  created: (r.data?.Created as string) ?? r.created_at ?? null,
137
141
  }))
138
- return { ok: true, tickets }
142
+ return { ok: true, tickets, blockId: ds.blockId, pageId: ds.pageId }
139
143
  } catch (err: any) {
140
144
  log(`listTickets failed for ${cwd}: ${err?.message || err}`)
141
145
  return { ok: false, tickets: [], error: err?.message || String(err) }
142
146
  }
143
147
  }
144
148
 
145
- private async ensureDatastore(cwd: string, create: boolean): Promise<string | null> {
146
- const cached = this.blockCache.get(cwd)
149
+ private async ensureDatastore(cwd: string, create: boolean): Promise<{ blockId: string; pageId: string } | null> {
150
+ const cached = this.dsCache.get(cwd)
147
151
  if (cached) return cached
148
152
 
149
153
  let folder: any = null
@@ -157,12 +161,14 @@ export class TicketStore {
157
161
  const folderDetail = await this.api.getFolder(folder.id)
158
162
  const agentPage = findPageByTitle(folderDetail?.pages || [], AGENT_DATASTORE_PAGE_TITLE)
159
163
  if (!agentPage?.id) return null
164
+ const pageId: string = agentPage.id
160
165
 
161
- const blockId = await this.findOrAdoptBlock(agentPage.id)
166
+ const blockId = await this.findOrAdoptBlock(pageId)
162
167
  if (blockId) {
163
168
  await this.ensureColumns(blockId)
164
- this.blockCache.set(cwd, blockId)
165
- return blockId
169
+ const ds = { blockId, pageId }
170
+ this.dsCache.set(cwd, ds)
171
+ return ds
166
172
  }
167
173
 
168
174
  if (!create) return null
@@ -173,15 +179,16 @@ export class TicketStore {
173
179
  type: c.type,
174
180
  ...(c.options ? { options: c.options } : {}),
175
181
  }))
176
- const created = await this.api.createBlock(agentPage.id, {
182
+ const created = await this.api.createBlock(pageId, {
177
183
  type: 'datastore',
178
184
  title: DATASTORE_TITLE,
179
185
  props: { columns, system_key: SYSTEM_KEY },
180
186
  })
181
187
  if (created?.id) {
182
188
  log(`Created "${DATASTORE_TITLE}" datastore on Agent Datastore page for ${cwd}`)
183
- this.blockCache.set(cwd, created.id)
184
- return created.id
189
+ const ds = { blockId: created.id, pageId }
190
+ this.dsCache.set(cwd, ds)
191
+ return ds
185
192
  }
186
193
  return null
187
194
  }
@@ -1,6 +1,7 @@
1
1
  import os from 'os'
2
2
  import { randomUUID } from 'crypto'
3
3
  import type { CtlsurfApi } from './ctlsurfApi'
4
+ import { log as baseLog } from './logger'
4
5
 
5
6
  const DATASTORE_TITLE = 'Time Tracking'
6
7
  const AGENT_DATASTORE_PAGE_TITLE = 'Agent Datastore'
@@ -56,7 +57,9 @@ function isSameLocalDay(a: number, b: number): boolean {
56
57
  }
57
58
 
58
59
  function log(...args: unknown[]): void {
59
- try { console.log('[time-tracker]', ...args) } catch { /* EPIPE safe */ }
60
+ // Route through the shared logger so terminal/TUI mode (setSilent) suppresses
61
+ // these — they'd otherwise corrupt the PTY display the agent owns.
62
+ baseLog('[time-tracker]', ...args)
60
63
  }
61
64
 
62
65
  function findPageByTitle(pages: any[], title: string): any | null {
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs'
2
2
  import path from 'path'
3
3
  import os from 'os'
4
+ import { log } from './logger'
4
5
 
5
6
  /**
6
7
  * Transcript Tailer
@@ -80,7 +81,7 @@ export class TranscriptTailer {
80
81
  this.sinceMs = Date.now()
81
82
  this.files.clear()
82
83
  this.pollTimer = setInterval(() => this.poll(), POLL_INTERVAL_MS)
83
- console.log(`[transcripts] Tailing ${this.agentId} transcripts for ${this.cwd}`)
84
+ log(`[transcripts] Tailing ${this.agentId} transcripts for ${this.cwd}`)
84
85
  }
85
86
 
86
87
  stop(): void {
@@ -90,7 +91,7 @@ export class TranscriptTailer {
90
91
  // Final drain so messages written just before exit aren't lost.
91
92
  this.poll()
92
93
  this.files.clear()
93
- console.log('[transcripts] Stopped')
94
+ log('[transcripts] Stopped')
94
95
  }
95
96
 
96
97
  private poll(): void {
@@ -83,6 +83,8 @@ const api = {
83
83
  // Clipboard — write a rich-HTML table (+ plain-text fallback) for email paste.
84
84
  copyEmailTable: (html: string, text: string): Promise<{ ok: boolean }> =>
85
85
  ipcRenderer.invoke('clipboard:writeTable', html, text),
86
+ copyText: (text: string): Promise<{ ok: boolean }> =>
87
+ ipcRenderer.invoke('clipboard:writeText', text),
86
88
  getVersion: (): Promise<string> =>
87
89
  ipcRenderer.invoke('app:getVersion'),
88
90
  getUpdateInfo: (): Promise<{ current: string; latest: string | null; hasUpdate: boolean; checkedAt: number | null }> =>
@@ -60,7 +60,7 @@ declare global {
60
60
  getTicketProject: () => Promise<{ cwd: string | null; name: string | null }>
61
61
  addTicket: (input: { title: string; description?: string; status?: string; priority?: string }) => Promise<{ ok: boolean; error?: string }>
62
62
  updateTicket: (rowId: string, input: { title: string; description?: string; status?: string; priority?: string }) => Promise<{ ok: boolean; error?: string }>
63
- listTickets: () => Promise<{ ok: boolean; tickets: Array<{ id: string; title: string; description: string; status: string; priority: string; created: string | null }>; error?: string }>
63
+ listTickets: () => Promise<{ ok: boolean; tickets: Array<{ id: string; title: string; description: string; status: string; priority: string; created: string | null }>; blockId?: string; pageId?: string; error?: string }>
64
64
  getTracking: () => Promise<{ active: boolean }>
65
65
  setTracking: (enabled: boolean) => Promise<{ active: boolean }>
66
66
  getLogChat: () => Promise<{ enabled: boolean }>
@@ -88,6 +88,7 @@ declare global {
88
88
  getUpdateInfo: () => Promise<UpdateInfo>
89
89
  onUpdateInfo: (callback: (info: UpdateInfo) => void) => () => void
90
90
  copyEmailTable: (html: string, text: string) => Promise<{ ok: boolean }>
91
+ copyText: (text: string) => Promise<{ ok: boolean }>
91
92
  }
92
93
  }
93
94
  }
@@ -269,6 +270,18 @@ export default function App() {
269
270
  setTimeout(() => window.worker.writePty(activeTabId, '\r'), 80)
270
271
  }, [activeTabId])
271
272
 
273
+ // Send a prepared prompt (e.g. a ticket) into the active terminal and submit
274
+ // it, so the coding agent can start working immediately. Same paste-then-
275
+ // delayed-\r dance as voice typing — a slightly longer delay since ticket
276
+ // prompts are multi-line and take a touch longer to drain before the Enter.
277
+ const handleSendToTerminal = useCallback((text: string) => {
278
+ const trimmed = text.trim()
279
+ if (!trimmed) return
280
+ window.worker.writePty(activeTabId, trimmed)
281
+ focusTerminal(activeTabId)
282
+ setTimeout(() => window.worker.writePty(activeTabId, '\r'), 120)
283
+ }, [activeTabId])
284
+
272
285
  const cwdRef = useRef<string | null>(null)
273
286
 
274
287
  const handleSpawn = useCallback(async (tabId: string, agent: AgentConfig) => {
@@ -510,7 +523,7 @@ export default function App() {
510
523
  ),
511
524
  },
512
525
  { id: 'ctlsurf', label: 'ctlsurf', content: <CtlsurfPanel navigate={ctlsurfNav} /> },
513
- { id: 'tickets', label: 'Tickets', content: <TicketPane /> },
526
+ { id: 'tickets', label: 'Tickets', content: <TicketPane onSendToTerminal={handleSendToTerminal} /> },
514
527
  ]
515
528
 
516
529
  return (