@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
@@ -46,6 +46,44 @@ function TagIcon({ size = 16 }: { size?: number }) {
46
46
  )
47
47
  }
48
48
 
49
+ // Paper-plane "send" glyph for the per-ticket dispatch button.
50
+ function SendIcon({ size = 15 }: { size?: number }) {
51
+ return (
52
+ <svg viewBox="0 0 24 24" width={size} height={size} fill="none"
53
+ stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
54
+ <line x1="22" y1="2" x2="11" y2="13" />
55
+ <polygon points="22 2 15 22 11 13 2 9 22 2" />
56
+ </svg>
57
+ )
58
+ }
59
+
60
+ interface TicketRefs { blockId: string; pageId: string }
61
+
62
+ // Formats a ticket into a prompt the coding agent can act on directly. Includes
63
+ // the ctlsurf identifiers (row / datastore block / page) so the agent can read
64
+ // the full ticket or flip its status via the ctlsurf MCP tools.
65
+ function ticketPrompt(t: Ticket, refs: TicketRefs | null): string {
66
+ const lines = [
67
+ 'Please work on this ticket:',
68
+ `Title: ${t.title || 'Untitled'}`,
69
+ `Priority: ${t.priority}`,
70
+ ]
71
+ const desc = t.description?.trim()
72
+ if (desc) lines.push('', desc)
73
+ lines.push(
74
+ '',
75
+ 'ctlsurf references (use the ctlsurf MCP tools to read details or update status):',
76
+ `- Ticket row ID: ${t.id}`,
77
+ )
78
+ if (refs) {
79
+ lines.push(
80
+ `- Tickets datastore block ID: ${refs.blockId}`,
81
+ `- Agent Datastore page ID: ${refs.pageId}`,
82
+ )
83
+ }
84
+ return lines.join('\n')
85
+ }
86
+
49
87
  interface SegmentedProps {
50
88
  options: Option[]
51
89
  value: string
@@ -75,40 +113,55 @@ function Segmented({ options, value, onChange }: SegmentedProps) {
75
113
  }
76
114
 
77
115
  // A single ticket row. Shared between the open list and the (collapsible)
78
- // Done section so both render identically.
79
- function TicketCard({ ticket, onClick }: { ticket: Ticket; onClick: () => void }) {
116
+ // Done section so both render identically. The card body opens the editor; the
117
+ // trailing send button dispatches the ticket straight to the coding terminal.
118
+ function TicketCard({ ticket, onClick, onSend }: { ticket: Ticket; onClick: () => void; onSend: () => void }) {
80
119
  return (
81
- <button className="ticket-card" onClick={onClick}>
82
- <span className="ticket-card-bar" style={{ background: colorOf(STATUS_OPTIONS, ticket.status) }} />
83
- <div className="ticket-card-main">
84
- <span className="ticket-card-title">{ticket.title || 'Untitled'}</span>
85
- <div className="ticket-card-meta">
86
- <span className="ticket-card-status" style={{ color: colorOf(STATUS_OPTIONS, ticket.status) }}>
87
- <span className="ticket-seg-dot" style={{ background: colorOf(STATUS_OPTIONS, ticket.status) }} />
88
- {ticket.status}
89
- </span>
90
- {ticket.created && <span className="ticket-card-date">{formatDate(ticket.created)}</span>}
120
+ <div className="ticket-card">
121
+ <button className="ticket-card-open" onClick={onClick} title="Edit ticket">
122
+ <span className="ticket-card-bar" style={{ background: colorOf(STATUS_OPTIONS, ticket.status) }} />
123
+ <div className="ticket-card-main">
124
+ <span className="ticket-card-title">{ticket.title || 'Untitled'}</span>
125
+ <div className="ticket-card-meta">
126
+ <span className="ticket-card-status" style={{ color: colorOf(STATUS_OPTIONS, ticket.status) }}>
127
+ <span className="ticket-seg-dot" style={{ background: colorOf(STATUS_OPTIONS, ticket.status) }} />
128
+ {ticket.status}
129
+ </span>
130
+ {ticket.created && <span className="ticket-card-date">{formatDate(ticket.created)}</span>}
131
+ </div>
91
132
  </div>
92
- </div>
93
- <span
94
- className="ticket-card-pri"
95
- style={{ color: colorOf(PRIORITY_OPTIONS, ticket.priority), borderColor: `${colorOf(PRIORITY_OPTIONS, ticket.priority)}66` }}
133
+ <span
134
+ className="ticket-card-pri"
135
+ style={{ color: colorOf(PRIORITY_OPTIONS, ticket.priority), borderColor: `${colorOf(PRIORITY_OPTIONS, ticket.priority)}66` }}
136
+ >
137
+ {ticket.priority}
138
+ </span>
139
+ </button>
140
+ <button
141
+ type="button"
142
+ className="ticket-card-send"
143
+ onClick={onSend}
144
+ title="Send to coding terminal"
145
+ aria-label="Send ticket to coding terminal"
96
146
  >
97
- {ticket.priority}
98
- </span>
99
- </button>
147
+ <SendIcon />
148
+ </button>
149
+ </div>
100
150
  )
101
151
  }
102
152
 
103
153
  // Docked side-pane variant of the ticket UI: header + list/form body + footer.
104
154
  // It only mounts when its pane is visible, so it loads on mount and refreshes
105
155
  // when the connected project (cwd) changes. Closed via the pane's own header.
106
- export function TicketPane() {
156
+ export function TicketPane({ onSendToTerminal }: { onSendToTerminal: (text: string) => void }) {
107
157
  const [project, setProject] = useState<string | null>(null)
108
158
  const [view, setView] = useState<View>('list')
109
159
 
110
160
  // list state
111
161
  const [tickets, setTickets] = useState<Ticket[]>([])
162
+ // ctlsurf identifiers for this project's Tickets datastore, sent along when
163
+ // dispatching a ticket so the agent can locate it via the MCP tools.
164
+ const [refs, setRefs] = useState<TicketRefs | null>(null)
112
165
  const [listLoading, setListLoading] = useState(false)
113
166
  const [listError, setListError] = useState<string | null>(null)
114
167
  // Done tickets are tucked into a collapsed section so the list stays focused
@@ -128,10 +181,13 @@ export function TicketPane() {
128
181
  setListError(null)
129
182
  try {
130
183
  const r = await window.worker.listTickets()
131
- if (r?.ok) setTickets(r.tickets || [])
132
- else { setTickets([]); setListError(r?.error || 'Failed to load tickets') }
184
+ if (r?.ok) {
185
+ setTickets(r.tickets || [])
186
+ setRefs(r.blockId && r.pageId ? { blockId: r.blockId, pageId: r.pageId } : null)
187
+ } else { setTickets([]); setRefs(null); setListError(r?.error || 'Failed to load tickets') }
133
188
  } catch (err: any) {
134
189
  setTickets([])
190
+ setRefs(null)
135
191
  setListError(err?.message || 'Failed to load tickets')
136
192
  } finally {
137
193
  setListLoading(false)
@@ -251,7 +307,7 @@ export function TicketPane() {
251
307
  </div>
252
308
  )}
253
309
  {!listLoading && activeTickets.map(t => (
254
- <TicketCard key={t.id} ticket={t} onClick={() => openEditForm(t)} />
310
+ <TicketCard key={t.id} ticket={t} onClick={() => openEditForm(t)} onSend={() => onSendToTerminal(ticketPrompt(t, refs))} />
255
311
  ))}
256
312
  {!listLoading && activeTickets.length === 0 && doneTickets.length > 0 && (
257
313
  <div className="ticket-list-msg">Nothing open — all caught up.</div>
@@ -269,7 +325,7 @@ export function TicketPane() {
269
325
  <span className="ticket-done-count">{doneTickets.length}</span>
270
326
  </button>
271
327
  {showDone && doneTickets.map(t => (
272
- <TicketCard key={t.id} ticket={t} onClick={() => openEditForm(t)} />
328
+ <TicketCard key={t.id} ticket={t} onClick={() => openEditForm(t)} onSend={() => onSendToTerminal(ticketPrompt(t, refs))} />
273
329
  ))}
274
330
  </>
275
331
  )}
@@ -78,7 +78,8 @@ function splitIntoChunks(text: string): string[] {
78
78
  // ─── Preferences ─────────────────────────────────────
79
79
 
80
80
  export function getEngine(): TtsEngineId {
81
- try { return localStorage.getItem(ENGINE_KEY) === 'neural' ? 'neural' : 'web' } catch { return 'web' }
81
+ // Default to the neural voice; only an explicit 'web' choice opts back out.
82
+ try { return localStorage.getItem(ENGINE_KEY) === 'web' ? 'web' : 'neural' } catch { return 'neural' }
82
83
  }
83
84
  export function setEngine(id: TtsEngineId): void {
84
85
  try { localStorage.setItem(ENGINE_KEY, id) } catch { /* ignore */ }
@@ -529,13 +529,88 @@ html, body, #root {
529
529
  width: 100%;
530
530
  height: 100%;
531
531
  overflow: hidden;
532
+ display: flex;
533
+ flex-direction: column;
534
+ }
535
+
536
+ .ctlsurf-addressbar {
537
+ flex: 0 0 auto;
538
+ display: flex;
539
+ align-items: center;
540
+ gap: 4px;
541
+ padding: 5px 6px;
542
+ background: #1a1b26;
543
+ border-bottom: 1px solid #2a2b3d;
544
+ }
545
+
546
+ .ctlsurf-nav-btn {
547
+ flex: 0 0 auto;
548
+ width: 24px;
549
+ height: 24px;
550
+ display: flex;
551
+ align-items: center;
552
+ justify-content: center;
553
+ padding: 0;
554
+ background: transparent;
555
+ border: none;
556
+ border-radius: 4px;
557
+ color: #a9b1d6;
558
+ font-size: 16px;
559
+ line-height: 1;
560
+ cursor: pointer;
561
+ }
562
+
563
+ .ctlsurf-nav-btn:hover:not(:disabled) {
564
+ background: #2a2b3d;
565
+ }
566
+
567
+ .ctlsurf-nav-btn:disabled {
568
+ color: #565f89;
569
+ opacity: 0.5;
570
+ cursor: default;
532
571
  }
533
572
 
534
- .ctlsurf-panel webview {
573
+ .ctlsurf-address-form {
574
+ flex: 1 1 auto;
575
+ display: flex;
576
+ min-width: 0;
577
+ }
578
+
579
+ .ctlsurf-address-input {
580
+ width: 100%;
581
+ height: 26px;
582
+ padding: 0 11px;
583
+ background: #16161e;
584
+ border: 1px solid #2a2b3d;
585
+ border-radius: 13px;
586
+ color: #c0caf5;
587
+ font-size: 12px;
588
+ outline: none;
589
+ }
590
+
591
+ .ctlsurf-address-input:focus {
592
+ border-color: #7aa2f7;
593
+ }
594
+
595
+ .ctlsurf-webview-host {
596
+ flex: 1 1 auto;
597
+ position: relative;
598
+ min-height: 0;
599
+ }
600
+
601
+ .ctlsurf-webview-host webview {
535
602
  width: 100%;
536
603
  height: 100%;
537
604
  }
538
605
 
606
+ .ctlsurf-webview-loading {
607
+ width: 100%;
608
+ height: 100%;
609
+ display: flex;
610
+ align-items: center;
611
+ justify-content: center;
612
+ }
613
+
539
614
  /* Status bar */
540
615
  .statusbar {
541
616
  height: 24px;
@@ -1541,6 +1616,22 @@ html, body, #root {
1541
1616
  font-size: 16px;
1542
1617
  }
1543
1618
 
1619
+ /* About section inside the Settings dialog. */
1620
+ .settings-about {
1621
+ border-top: 1px solid #2a2b3d;
1622
+ padding-top: 16px;
1623
+ }
1624
+ .settings-about .about-name {
1625
+ color: #c0caf5;
1626
+ font-size: 14px;
1627
+ font-weight: 600;
1628
+ }
1629
+ .settings-about .about-copyright {
1630
+ color: #9aa5ce;
1631
+ font-size: 12px;
1632
+ margin: 6px 0 8px;
1633
+ }
1634
+
1544
1635
  .settings-section {
1545
1636
  margin-bottom: 20px;
1546
1637
  }
@@ -2052,24 +2143,57 @@ html, body, #root {
2052
2143
  .ticket-card {
2053
2144
  display: flex;
2054
2145
  align-items: stretch;
2055
- gap: 10px;
2056
2146
  background: #15161f;
2057
2147
  border: 1px solid #262a3f;
2058
2148
  border-radius: 10px;
2059
- padding: 10px 11px 10px 0;
2060
- cursor: pointer;
2061
- text-align: left;
2062
- font-family: inherit;
2063
2149
  overflow: hidden;
2064
- transition: border-color 0.13s ease, background 0.13s ease, transform 0.06s ease;
2150
+ transition: border-color 0.13s ease, background 0.13s ease;
2065
2151
  }
2066
2152
  .ticket-card:hover {
2067
2153
  border-color: #3d425f;
2068
2154
  background: #181a26;
2069
2155
  }
2070
- .ticket-card:active {
2156
+ /* Clickable body of the card — opens the editor. */
2157
+ .ticket-card-open {
2158
+ flex: 1;
2159
+ min-width: 0;
2160
+ display: flex;
2161
+ align-items: stretch;
2162
+ gap: 10px;
2163
+ padding: 10px 11px 10px 0;
2164
+ background: transparent;
2165
+ border: none;
2166
+ color: inherit;
2167
+ cursor: pointer;
2168
+ text-align: left;
2169
+ font-family: inherit;
2170
+ transition: transform 0.06s ease;
2171
+ }
2172
+ .ticket-card-open:active {
2071
2173
  transform: scale(0.99);
2072
2174
  }
2175
+ /* Trailing button that dispatches the ticket to the coding terminal. */
2176
+ .ticket-card-send {
2177
+ flex-shrink: 0;
2178
+ width: 36px;
2179
+ align-self: stretch;
2180
+ display: flex;
2181
+ align-items: center;
2182
+ justify-content: center;
2183
+ background: transparent;
2184
+ border: none;
2185
+ border-left: 1px solid #262a3f;
2186
+ color: #565f89;
2187
+ cursor: pointer;
2188
+ transition: background 0.13s ease, color 0.13s ease, transform 0.06s ease;
2189
+ }
2190
+ .ticket-card-send:hover {
2191
+ background: #1b2030;
2192
+ color: #7aa2f7;
2193
+ }
2194
+ .ticket-card-send:active {
2195
+ transform: scale(0.9);
2196
+ }
2073
2197
  .ticket-card-bar {
2074
2198
  width: 3px;
2075
2199
  border-radius: 3px;
@@ -2144,6 +2268,106 @@ html, body, #root {
2144
2268
  .term-context-menu button:hover {
2145
2269
  background: #2a2b3d;
2146
2270
  }
2271
+ /* Always-visible utility button (top-right) — opens the snapshot panel. */
2272
+ .term-util-btn {
2273
+ position: absolute;
2274
+ top: 6px;
2275
+ right: 14px;
2276
+ z-index: 6;
2277
+ width: 26px;
2278
+ height: 26px;
2279
+ display: inline-flex;
2280
+ align-items: center;
2281
+ justify-content: center;
2282
+ border-radius: 6px;
2283
+ background: #2a2b3d;
2284
+ border: 1px solid #414868;
2285
+ color: #c0caf5;
2286
+ font-size: 16px;
2287
+ line-height: 1;
2288
+ cursor: pointer;
2289
+ opacity: 0.55;
2290
+ transition: opacity 0.13s ease, background 0.13s ease;
2291
+ }
2292
+ .term-util-btn:hover {
2293
+ opacity: 1;
2294
+ background: #343650;
2295
+ }
2296
+
2297
+ /* Snapshot panel: a frozen, selectable copy of the screen text. */
2298
+ .term-snapshot {
2299
+ position: absolute;
2300
+ inset: 0;
2301
+ z-index: 20;
2302
+ display: flex;
2303
+ flex-direction: column;
2304
+ background: #16161e;
2305
+ border: 1px solid #414868;
2306
+ }
2307
+ .term-snapshot-head {
2308
+ display: flex;
2309
+ align-items: center;
2310
+ justify-content: space-between;
2311
+ gap: 12px;
2312
+ padding: 8px 12px;
2313
+ background: #1f2335;
2314
+ border-bottom: 1px solid #414868;
2315
+ color: #9aa5ce;
2316
+ font-size: 12px;
2317
+ }
2318
+ .term-snapshot-x {
2319
+ background: transparent;
2320
+ border: none;
2321
+ color: #9aa5ce;
2322
+ font-size: 14px;
2323
+ cursor: pointer;
2324
+ padding: 2px 6px;
2325
+ border-radius: 4px;
2326
+ }
2327
+ .term-snapshot-x:hover {
2328
+ background: #2a2b3d;
2329
+ color: #c0caf5;
2330
+ }
2331
+ .term-snapshot-text {
2332
+ flex: 1;
2333
+ min-height: 0;
2334
+ overflow: auto;
2335
+ margin: 0;
2336
+ padding: 10px 12px;
2337
+ white-space: pre;
2338
+ user-select: text;
2339
+ cursor: text;
2340
+ color: #c0caf5;
2341
+ background: #16161e;
2342
+ font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', monospace;
2343
+ font-size: 13px;
2344
+ line-height: 1.4;
2345
+ }
2346
+ .term-snapshot-text::selection {
2347
+ background: #33467c;
2348
+ }
2349
+ .term-snapshot-actions {
2350
+ display: flex;
2351
+ gap: 6px;
2352
+ padding: 8px 12px;
2353
+ background: #1f2335;
2354
+ border-top: 1px solid #414868;
2355
+ }
2356
+ .term-snapshot-actions button {
2357
+ background: #2a2b3d;
2358
+ border: 1px solid #414868;
2359
+ border-radius: 5px;
2360
+ color: #c0caf5;
2361
+ font-size: 12.5px;
2362
+ padding: 6px 12px;
2363
+ cursor: pointer;
2364
+ }
2365
+ .term-snapshot-actions button:hover {
2366
+ background: #343650;
2367
+ }
2368
+ .term-snapshot-done {
2369
+ margin-left: auto;
2370
+ }
2147
2371
  .term-toast {
2148
2372
  position: fixed;
2149
2373
  bottom: 24px;