@phenx-inc/ctlsurf 0.11.0 → 0.12.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 (38) hide show
  1. package/out/headless/index.mjs +115 -5
  2. package/out/headless/index.mjs.map +3 -3
  3. package/out/main/index.js +316 -8
  4. package/out/preload/index.js +9 -0
  5. package/out/renderer/assets/{cssMode-Bmzy9hKX.js → cssMode-D7r66wvO.js} +3 -3
  6. package/out/renderer/assets/{freemarker2-CDAsExTa.js → freemarker2-jSWJIjPG.js} +1 -1
  7. package/out/renderer/assets/{handlebars-CvpBYG1L.js → handlebars-BnFyz5RH.js} +1 -1
  8. package/out/renderer/assets/{html-CNhEQqr8.js → html-Bl5UFavp.js} +1 -1
  9. package/out/renderer/assets/{htmlMode-DD7YPgga.js → htmlMode-D4VByBT6.js} +3 -3
  10. package/out/renderer/assets/{index-CGHeDwZa.css → index-Cc_TqOCL.css} +318 -0
  11. package/out/renderer/assets/{index-BDlCPzCG.js → index-DxhW1emk.js} +558 -85
  12. package/out/renderer/assets/{javascript-CsGLsswK.js → javascript-CMN1OmwG.js} +2 -2
  13. package/out/renderer/assets/{jsonMode-CTuGgRvC.js → jsonMode-CJ5AYSPV.js} +3 -3
  14. package/out/renderer/assets/{liquid-BrrN4uul.js → liquid-BGMIDTEm.js} +1 -1
  15. package/out/renderer/assets/{lspLanguageFeatures-Bhdn2Eqy.js → lspLanguageFeatures-DGifVYB5.js} +1 -1
  16. package/out/renderer/assets/{mdx-DJo-gax4.js → mdx-B2Z7R10Q.js} +1 -1
  17. package/out/renderer/assets/{python-DYHeg5b2.js → python-LMDMSSdn.js} +1 -1
  18. package/out/renderer/assets/{razor-Cqcks14K.js → razor-BZSrziPu.js} +1 -1
  19. package/out/renderer/assets/{tsMode-YTLgCayh.js → tsMode-DF9m6_3n.js} +1 -1
  20. package/out/renderer/assets/{typescript-oxBn_Vcw.js → typescript-rZeCMf0a.js} +1 -1
  21. package/out/renderer/assets/{xml-BEqGMCTc.js → xml-fNSZ-7so.js} +1 -1
  22. package/out/renderer/assets/{yaml-BGSrk5HC.js → yaml-1wxIuvzh.js} +1 -1
  23. package/out/renderer/index.html +2 -2
  24. package/package.json +1 -1
  25. package/src/main/agents.ts +9 -1
  26. package/src/main/ctlsurfApi.ts +7 -0
  27. package/src/main/index.ts +62 -4
  28. package/src/main/orchestrator.ts +112 -4
  29. package/src/main/ssh.ts +348 -0
  30. package/src/preload/index.ts +26 -0
  31. package/src/renderer/App.tsx +25 -1
  32. package/src/renderer/components/AgentPicker.tsx +18 -0
  33. package/src/renderer/components/ProjectPicker.tsx +121 -0
  34. package/src/renderer/components/RemoteConnect.tsx +315 -0
  35. package/src/renderer/components/SpeakControls.tsx +8 -2
  36. package/src/renderer/components/TerminalPanel.tsx +134 -23
  37. package/src/renderer/components/TicketPanel.tsx +67 -26
  38. package/src/renderer/styles.css +318 -0
@@ -74,6 +74,32 @@ function Segmented({ options, value, onChange }: SegmentedProps) {
74
74
  )
75
75
  }
76
76
 
77
+ // 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 }) {
80
+ 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>}
91
+ </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` }}
96
+ >
97
+ {ticket.priority}
98
+ </span>
99
+ </button>
100
+ )
101
+ }
102
+
77
103
  // Docked side-pane variant of the ticket UI: header + list/form body + footer.
78
104
  // It only mounts when its pane is visible, so it loads on mount and refreshes
79
105
  // when the connected project (cwd) changes. Closed via the pane's own header.
@@ -85,6 +111,9 @@ export function TicketPane() {
85
111
  const [tickets, setTickets] = useState<Ticket[]>([])
86
112
  const [listLoading, setListLoading] = useState(false)
87
113
  const [listError, setListError] = useState<string | null>(null)
114
+ // Done tickets are tucked into a collapsed section so the list stays focused
115
+ // on what's still open; the user can expand to review completed work.
116
+ const [showDone, setShowDone] = useState(false)
88
117
 
89
118
  // form state
90
119
  const [editingId, setEditingId] = useState<string | null>(null)
@@ -178,6 +207,10 @@ export function TicketPane() {
178
207
  return () => window.removeEventListener('keydown', onKey)
179
208
  }, [view, handleSave])
180
209
 
210
+ // Split out Done tickets so they can live in their own collapsed section.
211
+ const activeTickets = tickets.filter(t => t.status !== 'Done')
212
+ const doneTickets = tickets.filter(t => t.status === 'Done')
213
+
181
214
  return (
182
215
  <div className="ticket-pane" role="region" aria-label="Tickets">
183
216
  <div className="ticket-drawer-header">
@@ -198,6 +231,9 @@ export function TicketPane() {
198
231
  </span>
199
232
  </div>
200
233
  </div>
234
+ {view === 'list' && (
235
+ <button className="ticket-drawer-new" onClick={openNewForm} title="New ticket">+ New</button>
236
+ )}
201
237
  </div>
202
238
 
203
239
  {view === 'list' ? (
@@ -214,34 +250,39 @@ export function TicketPane() {
214
250
  <span className="ticket-empty-hint">Log the first one for this project.</span>
215
251
  </div>
216
252
  )}
217
- {!listLoading && tickets.map(t => (
218
- <button key={t.id} className="ticket-card" onClick={() => openEditForm(t)}>
219
- <span className="ticket-card-bar" style={{ background: colorOf(STATUS_OPTIONS, t.status) }} />
220
- <div className="ticket-card-main">
221
- <span className="ticket-card-title">{t.title || 'Untitled'}</span>
222
- <div className="ticket-card-meta">
223
- <span className="ticket-card-status" style={{ color: colorOf(STATUS_OPTIONS, t.status) }}>
224
- <span className="ticket-seg-dot" style={{ background: colorOf(STATUS_OPTIONS, t.status) }} />
225
- {t.status}
226
- </span>
227
- {t.created && <span className="ticket-card-date">{formatDate(t.created)}</span>}
228
- </div>
229
- </div>
230
- <span
231
- className="ticket-card-pri"
232
- style={{ color: colorOf(PRIORITY_OPTIONS, t.priority), borderColor: `${colorOf(PRIORITY_OPTIONS, t.priority)}66` }}
233
- >
234
- {t.priority}
235
- </span>
236
- </button>
253
+ {!listLoading && activeTickets.map(t => (
254
+ <TicketCard key={t.id} ticket={t} onClick={() => openEditForm(t)} />
237
255
  ))}
256
+ {!listLoading && activeTickets.length === 0 && doneTickets.length > 0 && (
257
+ <div className="ticket-list-msg">Nothing open — all caught up.</div>
258
+ )}
259
+ {!listLoading && doneTickets.length > 0 && (
260
+ <>
261
+ <button
262
+ type="button"
263
+ className="ticket-done-toggle"
264
+ onClick={() => setShowDone(v => !v)}
265
+ aria-expanded={showDone}
266
+ >
267
+ <span className={`ticket-done-caret ${showDone ? 'open' : ''}`} aria-hidden="true">▸</span>
268
+ <span className="ticket-done-label">Done</span>
269
+ <span className="ticket-done-count">{doneTickets.length}</span>
270
+ </button>
271
+ {showDone && doneTickets.map(t => (
272
+ <TicketCard key={t.id} ticket={t} onClick={() => openEditForm(t)} />
273
+ ))}
274
+ </>
275
+ )}
238
276
  </div>
239
- <div className="ticket-drawer-footer">
240
- <span className="ticket-drawer-hint">
241
- {tickets.length > 0 && `${tickets.length} ticket${tickets.length === 1 ? '' : 's'}`}
242
- </span>
243
- <button className="ticket-btn-primary" onClick={openNewForm}>+ New ticket</button>
244
- </div>
277
+ {tickets.length > 0 && (
278
+ <div className="ticket-drawer-footer">
279
+ <span className="ticket-drawer-hint">
280
+ {doneTickets.length > 0
281
+ ? `${activeTickets.length} open · ${doneTickets.length} done`
282
+ : `${tickets.length} ticket${tickets.length === 1 ? '' : 's'}`}
283
+ </span>
284
+ </div>
285
+ )}
245
286
  </>
246
287
  ) : (
247
288
  <>
@@ -100,6 +100,17 @@ html, body, #root {
100
100
  background: #1f2335;
101
101
  }
102
102
 
103
+ .titlebar-btn:disabled {
104
+ opacity: 0.4;
105
+ cursor: not-allowed;
106
+ }
107
+
108
+ /* Keep the greyed-out look stable on hover for disabled buttons. */
109
+ .titlebar-btn:disabled:hover {
110
+ color: #565f89;
111
+ border-color: #3b3d57;
112
+ }
113
+
103
114
  .titlebar-separator {
104
115
  width: 1px;
105
116
  height: 16px;
@@ -266,6 +277,15 @@ html, body, #root {
266
277
  }
267
278
 
268
279
  /* Terminal container */
280
+ .terminal-wrap {
281
+ position: relative;
282
+ flex: 1;
283
+ min-width: 0;
284
+ min-height: 0;
285
+ width: 100%;
286
+ height: 100%;
287
+ }
288
+
269
289
  .terminal-container {
270
290
  width: 100%;
271
291
  height: 100%;
@@ -277,6 +297,34 @@ html, body, #root {
277
297
  height: 100%;
278
298
  }
279
299
 
300
+ /* Floating "jump to latest" pill — shown only while scrolled up. */
301
+ .term-jump-latest {
302
+ position: absolute;
303
+ bottom: 14px;
304
+ right: 20px;
305
+ z-index: 5;
306
+ display: inline-flex;
307
+ align-items: center;
308
+ gap: 5px;
309
+ padding: 5px 12px;
310
+ border-radius: 14px;
311
+ background: #2a2b3d;
312
+ border: 1px solid #7aa2f7;
313
+ color: #c0caf5;
314
+ font-family: inherit;
315
+ font-size: 11.5px;
316
+ font-weight: 600;
317
+ cursor: pointer;
318
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45);
319
+ transition: background 0.13s ease, transform 0.06s ease;
320
+ }
321
+ .term-jump-latest:hover {
322
+ background: #343650;
323
+ }
324
+ .term-jump-latest:active {
325
+ transform: translateY(1px);
326
+ }
327
+
280
328
  .terminal-fullscreen {
281
329
  width: 100%;
282
330
  height: 100%;
@@ -1150,6 +1198,9 @@ html, body, #root {
1150
1198
  border-radius: 12px;
1151
1199
  padding: 32px 40px;
1152
1200
  min-width: 360px;
1201
+ max-width: 460px;
1202
+ max-height: 88vh;
1203
+ overflow-y: auto;
1153
1204
  text-align: center;
1154
1205
  }
1155
1206
 
@@ -1218,6 +1269,7 @@ html, body, #root {
1218
1269
  display: flex;
1219
1270
  align-items: center;
1220
1271
  justify-content: space-between;
1272
+ gap: 16px;
1221
1273
  padding: 14px 18px;
1222
1274
  background: #16161e;
1223
1275
  border: 1px solid #2a2b3d;
@@ -1236,11 +1288,13 @@ html, body, #root {
1236
1288
  font-size: 14px;
1237
1289
  font-weight: 600;
1238
1290
  color: #c0caf5;
1291
+ flex-shrink: 0;
1239
1292
  }
1240
1293
 
1241
1294
  .agent-picker-desc {
1242
1295
  font-size: 11px;
1243
1296
  color: #565f89;
1297
+ text-align: right;
1244
1298
  }
1245
1299
 
1246
1300
  .agent-picker-hint {
@@ -1265,6 +1319,201 @@ html, body, #root {
1265
1319
  accent-color: #7aa2f7;
1266
1320
  }
1267
1321
 
1322
+ /* ─── Remote (SSH) connect ───────────────────────── */
1323
+
1324
+ .remote-section-label {
1325
+ font-size: 10px;
1326
+ color: #565f89;
1327
+ text-transform: uppercase;
1328
+ letter-spacing: 1px;
1329
+ text-align: left;
1330
+ margin: 16px 0 8px;
1331
+ }
1332
+
1333
+ /* Scroll the host list rather than letting it grow the whole modal. */
1334
+ .remote-host-scroll {
1335
+ max-height: 40vh;
1336
+ overflow-y: auto;
1337
+ padding-right: 4px;
1338
+ }
1339
+
1340
+ /* Project rows: title on top, path muted + truncated below (paths are long). */
1341
+ .project-item {
1342
+ flex-direction: column;
1343
+ align-items: flex-start;
1344
+ gap: 4px;
1345
+ }
1346
+
1347
+ .project-item .agent-picker-name {
1348
+ max-width: 100%;
1349
+ overflow: hidden;
1350
+ text-overflow: ellipsis;
1351
+ white-space: nowrap;
1352
+ }
1353
+
1354
+ .project-item .agent-picker-desc {
1355
+ text-align: left;
1356
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
1357
+ font-size: 10px;
1358
+ max-width: 100%;
1359
+ overflow: hidden;
1360
+ text-overflow: ellipsis;
1361
+ white-space: nowrap;
1362
+ }
1363
+
1364
+ /* Compact host rows so more fit before scrolling. */
1365
+ .remote-host-scroll .agent-picker-item {
1366
+ padding: 10px 14px;
1367
+ }
1368
+
1369
+ .remote-toggle {
1370
+ display: flex;
1371
+ align-items: center;
1372
+ gap: 8px;
1373
+ margin-bottom: 4px;
1374
+ }
1375
+
1376
+ .remote-toggle-label {
1377
+ font-size: 12px;
1378
+ color: #565f89;
1379
+ }
1380
+
1381
+ .remote-toggle-btn {
1382
+ flex: 1;
1383
+ padding: 8px 10px;
1384
+ background: #16161e;
1385
+ border: 1px solid #2a2b3d;
1386
+ border-radius: 8px;
1387
+ color: #a9b1d6;
1388
+ font-size: 12px;
1389
+ cursor: pointer;
1390
+ transition: all 0.15s;
1391
+ }
1392
+
1393
+ .remote-toggle-btn:hover {
1394
+ border-color: #7aa2f7;
1395
+ }
1396
+
1397
+ .remote-toggle-btn.is-active {
1398
+ border-color: #7aa2f7;
1399
+ background: #1a1b2e;
1400
+ color: #c0caf5;
1401
+ font-weight: 600;
1402
+ }
1403
+
1404
+ .remote-manual {
1405
+ display: flex;
1406
+ gap: 8px;
1407
+ }
1408
+
1409
+ .remote-input {
1410
+ flex: 1;
1411
+ padding: 10px 12px;
1412
+ background: #16161e;
1413
+ border: 1px solid #2a2b3d;
1414
+ border-radius: 8px;
1415
+ color: #c0caf5;
1416
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
1417
+ font-size: 12px;
1418
+ outline: none;
1419
+ }
1420
+
1421
+ .remote-input:focus {
1422
+ border-color: #7aa2f7;
1423
+ }
1424
+
1425
+ .remote-btn {
1426
+ padding: 10px 16px;
1427
+ background: #7aa2f7;
1428
+ border: none;
1429
+ border-radius: 8px;
1430
+ color: #1f2335;
1431
+ font-size: 12px;
1432
+ font-weight: 600;
1433
+ cursor: pointer;
1434
+ }
1435
+
1436
+ .remote-btn:disabled {
1437
+ opacity: 0.4;
1438
+ cursor: default;
1439
+ }
1440
+
1441
+ .remote-link {
1442
+ margin-top: 18px;
1443
+ background: none;
1444
+ border: none;
1445
+ color: #7aa2f7;
1446
+ font-size: 12px;
1447
+ cursor: pointer;
1448
+ }
1449
+
1450
+ .remote-status {
1451
+ padding: 24px 8px;
1452
+ color: #a9b1d6;
1453
+ font-size: 13px;
1454
+ }
1455
+
1456
+ .remote-status-sub {
1457
+ margin-top: 6px;
1458
+ font-size: 11px;
1459
+ color: #565f89;
1460
+ word-break: break-word;
1461
+ }
1462
+
1463
+ .remote-spinner {
1464
+ width: 22px;
1465
+ height: 22px;
1466
+ margin: 0 auto 14px;
1467
+ border: 2px solid #2a2b3d;
1468
+ border-top-color: #7aa2f7;
1469
+ border-radius: 50%;
1470
+ animation: remote-spin 0.8s linear infinite;
1471
+ }
1472
+
1473
+ @keyframes remote-spin {
1474
+ to { transform: rotate(360deg); }
1475
+ }
1476
+
1477
+ .remote-target-line {
1478
+ font-size: 13px;
1479
+ color: #a9b1d6;
1480
+ margin-bottom: 4px;
1481
+ }
1482
+
1483
+ .remote-note {
1484
+ font-size: 11px;
1485
+ color: #e0af68;
1486
+ background: rgba(224, 175, 104, 0.08);
1487
+ border: 1px solid rgba(224, 175, 104, 0.25);
1488
+ border-radius: 8px;
1489
+ padding: 8px 12px;
1490
+ margin: 8px 0;
1491
+ text-align: left;
1492
+ line-height: 1.4;
1493
+ }
1494
+
1495
+ .remote-error {
1496
+ font-size: 13px;
1497
+ color: #f7768e;
1498
+ margin-bottom: 4px;
1499
+ }
1500
+
1501
+ .remote-pw {
1502
+ margin: 8px 0 4px;
1503
+ }
1504
+
1505
+ .remote-pw .remote-input {
1506
+ width: 100%;
1507
+ box-sizing: border-box;
1508
+ }
1509
+
1510
+ .remote-pw-hint {
1511
+ margin-top: 6px;
1512
+ font-size: 10px;
1513
+ color: #565f89;
1514
+ text-align: left;
1515
+ }
1516
+
1268
1517
  /* Settings dialog */
1269
1518
  .settings-overlay {
1270
1519
  position: fixed;
@@ -1511,6 +1760,32 @@ html, body, #root {
1511
1760
  color: #7aa2f7;
1512
1761
  font-weight: 600;
1513
1762
  }
1763
+ /* "+ New" action in the header (replaces the old footer button). */
1764
+ .ticket-drawer-new {
1765
+ flex-shrink: 0;
1766
+ align-self: center;
1767
+ display: inline-flex;
1768
+ align-items: center;
1769
+ gap: 3px;
1770
+ padding: 5px 11px;
1771
+ border-radius: 7px;
1772
+ background: rgba(122, 162, 247, 0.14);
1773
+ border: 1px solid rgba(122, 162, 247, 0.3);
1774
+ color: #9db4f5;
1775
+ font-family: inherit;
1776
+ font-size: 12px;
1777
+ font-weight: 600;
1778
+ cursor: pointer;
1779
+ transition: background 0.13s ease, border-color 0.13s ease, color 0.13s ease, transform 0.06s ease;
1780
+ }
1781
+ .ticket-drawer-new:hover {
1782
+ background: rgba(122, 162, 247, 0.22);
1783
+ border-color: rgba(122, 162, 247, 0.5);
1784
+ color: #c0d0fb;
1785
+ }
1786
+ .ticket-drawer-new:active {
1787
+ transform: translateY(1px);
1788
+ }
1514
1789
  .ticket-drawer-warn {
1515
1790
  color: #e0af68;
1516
1791
  }
@@ -1708,6 +1983,49 @@ html, body, #root {
1708
1983
  text-align: center;
1709
1984
  }
1710
1985
 
1986
+ /* ── collapsible "Done" section header ── */
1987
+ .ticket-done-toggle {
1988
+ display: flex;
1989
+ align-items: center;
1990
+ gap: 7px;
1991
+ width: 100%;
1992
+ margin-top: 3px;
1993
+ padding: 7px 4px;
1994
+ background: none;
1995
+ border: none;
1996
+ border-top: 1px solid #262a3f;
1997
+ color: #6b739b;
1998
+ font-family: inherit;
1999
+ font-size: 11.5px;
2000
+ font-weight: 600;
2001
+ letter-spacing: 0.03em;
2002
+ text-transform: uppercase;
2003
+ cursor: pointer;
2004
+ transition: color 0.13s ease;
2005
+ }
2006
+ .ticket-done-toggle:hover {
2007
+ color: #a9b1d6;
2008
+ }
2009
+ .ticket-done-caret {
2010
+ display: inline-block;
2011
+ font-size: 10px;
2012
+ transition: transform 0.13s ease;
2013
+ }
2014
+ .ticket-done-caret.open {
2015
+ transform: rotate(90deg);
2016
+ }
2017
+ .ticket-done-count {
2018
+ margin-left: auto;
2019
+ min-width: 18px;
2020
+ padding: 1px 6px;
2021
+ border-radius: 9px;
2022
+ background: #262a3f;
2023
+ color: #8088ac;
2024
+ font-size: 11px;
2025
+ font-weight: 600;
2026
+ text-align: center;
2027
+ }
2028
+
1711
2029
  .ticket-empty {
1712
2030
  display: flex;
1713
2031
  flex-direction: column;