@phenx-inc/ctlsurf 0.13.5 → 0.14.0

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 (44) hide show
  1. package/out/headless/index.mjs +13663 -385
  2. package/out/headless/index.mjs.map +4 -4
  3. package/out/main/index.js +11677 -290
  4. package/out/main/node-CUnQxFha.js +868 -0
  5. package/out/preload/index.js +19 -0
  6. package/out/renderer/assets/{cssMode-CAEObGNt.js → cssMode-7adWSORr.js} +3 -3
  7. package/out/renderer/assets/{freemarker2-D63hnnDq.js → freemarker2-BkLLWnzk.js} +1 -1
  8. package/out/renderer/assets/{handlebars-UYo_IYpi.js → handlebars-Cinfm9fZ.js} +1 -1
  9. package/out/renderer/assets/{html-2dvYDi3g.js → html-vl9fkHKL.js} +1 -1
  10. package/out/renderer/assets/{htmlMode-B6fxDDPF.js → htmlMode-BtGqkTNI.js} +3 -3
  11. package/out/renderer/assets/{index-CMb4AqkZ.js → index-BhrV6ld-.js} +381 -29
  12. package/out/renderer/assets/{index-BEekQe3-.css → index-BpCORklm.css} +217 -0
  13. package/out/renderer/assets/{javascript-8RyFDDdk.js → javascript-B7CCdfuW.js} +2 -2
  14. package/out/renderer/assets/{jsonMode-CqspkWCb.js → jsonMode-ut9LWBMJ.js} +3 -3
  15. package/out/renderer/assets/{liquid-DaEnAsYY.js → liquid-DVDJ0lKt.js} +1 -1
  16. package/out/renderer/assets/{lspLanguageFeatures-Czkp5sZb.js → lspLanguageFeatures-BbMj8Wsc.js} +1 -1
  17. package/out/renderer/assets/{mdx-Q-qSZsFg.js → mdx-CcQzYB0J.js} +1 -1
  18. package/out/renderer/assets/{python-Sw0S0BJV.js → python-Byy7wKMv.js} +1 -1
  19. package/out/renderer/assets/{razor-C7zQmabz.js → razor-D1xi_wMM.js} +1 -1
  20. package/out/renderer/assets/{tsMode-BWdOxk6v.js → tsMode-DJPzbn5i.js} +1 -1
  21. package/out/renderer/assets/{typescript-BrdIiKZf.js → typescript-BuVaU7i3.js} +1 -1
  22. package/out/renderer/assets/{xml-DbKLNPOq.js → xml-rzc91O3d.js} +1 -1
  23. package/out/renderer/assets/{yaml-ehupJBsZ.js → yaml-fAOzQMge.js} +1 -1
  24. package/out/renderer/index.html +2 -2
  25. package/package.json +2 -1
  26. package/src/main/index.ts +55 -8
  27. package/src/main/observer/anthropic.ts +48 -0
  28. package/src/main/observer/budget.ts +79 -0
  29. package/src/main/observer/ledger.ts +61 -0
  30. package/src/main/observer/observer.ts +566 -0
  31. package/src/main/observer/spans.ts +170 -0
  32. package/src/main/observer/store.ts +200 -0
  33. package/src/main/observer/tier0.ts +234 -0
  34. package/src/main/observer/tier1.ts +306 -0
  35. package/src/main/observer/tier2.ts +125 -0
  36. package/src/main/observer/types.ts +224 -0
  37. package/src/main/orchestrator.ts +74 -0
  38. package/src/main/transcripts.ts +18 -0
  39. package/src/preload/index.ts +40 -2
  40. package/src/renderer/App.tsx +22 -0
  41. package/src/renderer/components/ObserverPanel.tsx +289 -0
  42. package/src/renderer/components/SettingsDialog.tsx +104 -0
  43. package/src/renderer/components/TerminalPanel.tsx +28 -5
  44. package/src/renderer/styles.css +217 -0
@@ -35,21 +35,60 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
35
35
  const [saved, setSaved] = useState(false)
36
36
  const [version, setVersion] = useState('')
37
37
 
38
+ // Observer (global) — the key is write-only over IPC; we only learn
39
+ // whether one is configured (hasApiKey) and from where (keySource).
40
+ const [obsEnabled, setObsEnabled] = useState(false)
41
+ const [obsApiKey, setObsApiKey] = useState('')
42
+ const [obsHasKey, setObsHasKey] = useState(false)
43
+ const [obsKeySource, setObsKeySource] = useState<'settings' | 'env' | null>(null)
44
+ const [obsCapUsd, setObsCapUsd] = useState(1)
45
+ const [obsSaved, setObsSaved] = useState(false)
46
+
38
47
  const loadProfiles = async () => {
39
48
  const data = await window.worker.listProfiles() as ProfileListResponse
40
49
  setProfileList(data)
41
50
  setActiveProfileId(data.activeProfile)
42
51
  }
43
52
 
53
+ const loadObserver = async () => {
54
+ try {
55
+ const o = await window.worker.getObserverSettings()
56
+ setObsEnabled(o.enabled)
57
+ setObsHasKey(o.hasApiKey)
58
+ setObsKeySource(o.keySource)
59
+ setObsCapUsd(o.dailyCapUsd)
60
+ setObsApiKey('')
61
+ } catch { /* older main process without observer IPC */ }
62
+ }
63
+
44
64
  useEffect(() => {
45
65
  if (open) {
46
66
  loadProfiles()
67
+ loadObserver()
47
68
  setEditingId(null)
48
69
  setSaved(false)
70
+ setObsSaved(false)
49
71
  window.worker.getVersion().then(setVersion).catch(() => { /* ignore */ })
50
72
  }
51
73
  }, [open])
52
74
 
75
+ const handleObserverSave = async () => {
76
+ const data: { enabled: boolean; anthropicApiKey?: string; dailyCapUsd?: number } = {
77
+ enabled: obsEnabled,
78
+ dailyCapUsd: Math.max(0.05, obsCapUsd) || 1,
79
+ }
80
+ // Only send the key if the user typed a new one (blank = keep stored).
81
+ if (obsApiKey.trim()) {
82
+ data.anthropicApiKey = obsApiKey.trim()
83
+ }
84
+ const o = await window.worker.setObserverSettings(data)
85
+ setObsHasKey(o.hasApiKey)
86
+ setObsKeySource(o.keySource)
87
+ setObsApiKey('')
88
+ setObsSaved(true)
89
+ setTimeout(() => setObsSaved(false), 900)
90
+ }
91
+
53
92
  const startEditing = (profile: ProfileSummary) => {
54
93
  setEditingId(profile.id)
55
94
  setName(profile.name)
@@ -238,6 +277,71 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
238
277
  </div>
239
278
  )}
240
279
 
280
+ {!editingId && (
281
+ // ─── Observer (global) ─────────────────
282
+ <div className="settings-section">
283
+ <h4>Observer</h4>
284
+ <p className="settings-hint">
285
+ Reviews agent sessions for assumptions, unverified claims, and
286
+ dependency/license risk, tracks time per work item, and posts a
287
+ session digest to the project's Agent Datastore page.
288
+ </p>
289
+
290
+ <label className="settings-checkbox">
291
+ <input
292
+ type="checkbox"
293
+ checked={obsEnabled}
294
+ onChange={e => setObsEnabled(e.target.checked)}
295
+ />
296
+ <span>Enable Observer</span>
297
+ <span className="settings-hint">
298
+ Applies to newly started agent tabs. Advisory only — never
299
+ blocks or slows the agent.
300
+ </span>
301
+ </label>
302
+
303
+ <label>
304
+ Anthropic API Key
305
+ <input
306
+ type="password"
307
+ value={obsApiKey}
308
+ onChange={e => setObsApiKey(e.target.value)}
309
+ placeholder={
310
+ obsKeySource === 'settings' ? '***configured*** (leave blank to keep)'
311
+ : obsKeySource === 'env' ? 'using ANTHROPIC_API_KEY from environment'
312
+ : 'sk-ant-…'
313
+ }
314
+ />
315
+ <span className="settings-hint">
316
+ Your own Anthropic key — separate from the ctlsurf profile key.
317
+ Without one, the Observer still records the session ledger,
318
+ Tier-0 tags, and spans, but skips model review and digests.
319
+ </span>
320
+ </label>
321
+
322
+ <label>
323
+ Daily budget cap (USD)
324
+ <input
325
+ type="number"
326
+ min={0.05}
327
+ step={0.25}
328
+ value={obsCapUsd}
329
+ onChange={e => setObsCapUsd(parseFloat(e.target.value) || 1)}
330
+ />
331
+ <span className="settings-hint">
332
+ Model review pauses for the day once spend reaches the cap;
333
+ Tier-0 tagging and spans continue.
334
+ </span>
335
+ </label>
336
+
337
+ <div className="settings-actions">
338
+ <button className="btn-primary" onClick={handleObserverSave}>
339
+ {obsSaved ? 'Saved!' : 'Save Observer Settings'}
340
+ </button>
341
+ </div>
342
+ </div>
343
+ )}
344
+
241
345
  <div className="settings-section settings-about">
242
346
  <h4>About</h4>
243
347
  <div className="about-name">ctlsurf{version ? ` · v${version}` : ''}</div>
@@ -17,18 +17,41 @@ function showTerminalToast(message: string): void {
17
17
 
18
18
  function copyTextValue(text: string): void {
19
19
  if (!text) return
20
+ // Toast only after the main process confirms the write actually landed
21
+ // (read-back verified). A silent clipboard failure used to still show
22
+ // "Copied to clipboard", masking the problem.
20
23
  void window.worker.copyText(text)
21
- showTerminalToast('Copied to clipboard')
24
+ .then((res) => {
25
+ if (res?.ok) showTerminalToast('Copied to clipboard')
26
+ else {
27
+ showTerminalToast('Copy failed — clipboard not updated')
28
+ console.warn('[copy] clipboard write not verified', res)
29
+ }
30
+ })
31
+ .catch((err) => {
32
+ showTerminalToast('Copy failed')
33
+ console.warn('[copy] clipboard write threw', err)
34
+ })
22
35
  }
23
36
 
24
37
  function convertTextToTable(text: string): void {
25
38
  const result = convertToEmailTable(text)
26
- if (result) {
27
- window.worker.copyEmailTable(result.html, result.text)
28
- showTerminalToast(`Copied ${result.rows}×${result.cols} table to clipboard`)
29
- } else {
39
+ if (!result) {
30
40
  showTerminalToast('No table found in selection')
41
+ return
31
42
  }
43
+ void window.worker.copyEmailTable(result.html, result.text)
44
+ .then((res) => {
45
+ if (res?.ok) showTerminalToast(`Copied ${result.rows}×${result.cols} table to clipboard`)
46
+ else {
47
+ showTerminalToast('Copy failed — clipboard not updated')
48
+ console.warn('[copy] table clipboard write not verified', res)
49
+ }
50
+ })
51
+ .catch((err) => {
52
+ showTerminalToast('Copy failed')
53
+ console.warn('[copy] table clipboard write threw', err)
54
+ })
32
55
  }
33
56
 
34
57
  // unlock() is called from the triggering gesture so the neural voice isn't
@@ -2382,3 +2382,220 @@ html, body, #root {
2382
2382
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
2383
2383
  pointer-events: none;
2384
2384
  }
2385
+
2386
+ /* ─── Observer pane ─────────────────────────────── */
2387
+
2388
+ /* Header on/off switch */
2389
+ .observer-switch { display: inline-flex; align-items: center; cursor: pointer; padding-top: 2px; }
2390
+ .observer-switch input { position: absolute; opacity: 0; width: 0; height: 0; }
2391
+ .observer-switch-track {
2392
+ width: 34px; height: 19px; border-radius: 10px;
2393
+ background: #3b4261; position: relative; transition: background 0.15s ease;
2394
+ display: inline-block; flex-shrink: 0;
2395
+ }
2396
+ .observer-switch-thumb {
2397
+ position: absolute; top: 2px; left: 2px; width: 15px; height: 15px;
2398
+ border-radius: 50%; background: #c0caf5; transition: transform 0.15s ease;
2399
+ }
2400
+ .observer-switch input:checked + .observer-switch-track { background: #9ece6a; }
2401
+ .observer-switch input:checked + .observer-switch-track .observer-switch-thumb {
2402
+ transform: translateX(15px); background: #1a1b26;
2403
+ }
2404
+ .observer-switch input:disabled + .observer-switch-track { opacity: 0.5; }
2405
+
2406
+ .obs-feed { gap: 6px; }
2407
+
2408
+ /* Finding / digest cards */
2409
+ .obs-card {
2410
+ position: relative;
2411
+ display: flex;
2412
+ flex-direction: column;
2413
+ gap: 5px;
2414
+ width: 100%;
2415
+ text-align: left;
2416
+ appearance: none;
2417
+ font: inherit;
2418
+ color: inherit;
2419
+ cursor: pointer;
2420
+ background: #15161f;
2421
+ border: 1px solid #262a3f;
2422
+ border-left: 3px solid #565f89;
2423
+ border-radius: 9px;
2424
+ padding: 9px 26px 9px 12px;
2425
+ transition: border-color 0.12s ease, background 0.12s ease;
2426
+ }
2427
+ .obs-card:hover { background: #181a26; border-color: #323752; border-left-width: 3px; }
2428
+ .obs-card:active { transform: scale(0.995); }
2429
+
2430
+ .obs-card-top {
2431
+ display: flex;
2432
+ align-items: center;
2433
+ gap: 8px;
2434
+ min-width: 0;
2435
+ }
2436
+ .obs-chip {
2437
+ display: inline-flex;
2438
+ align-items: center;
2439
+ gap: 5px;
2440
+ flex-shrink: 0;
2441
+ font-size: 9.5px;
2442
+ font-weight: 600;
2443
+ letter-spacing: 0.06em;
2444
+ text-transform: uppercase;
2445
+ padding: 2px 7px 2px 6px;
2446
+ border: 1px solid;
2447
+ border-radius: 999px;
2448
+ line-height: 1.4;
2449
+ }
2450
+ .obs-chip-dot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }
2451
+ .obs-card-cat {
2452
+ font-size: 10px;
2453
+ color: #565f89;
2454
+ letter-spacing: 0.05em;
2455
+ text-transform: uppercase;
2456
+ white-space: nowrap;
2457
+ overflow: hidden;
2458
+ text-overflow: ellipsis;
2459
+ }
2460
+ .obs-card-time {
2461
+ margin-left: auto;
2462
+ flex-shrink: 0;
2463
+ font-size: 10px;
2464
+ color: #414868;
2465
+ font-variant-numeric: tabular-nums;
2466
+ }
2467
+ .obs-card-title {
2468
+ font-size: 12.5px;
2469
+ font-weight: 600;
2470
+ color: #c0caf5;
2471
+ line-height: 1.4;
2472
+ word-break: break-word;
2473
+ }
2474
+ .obs-card-body {
2475
+ font-size: 11.5px;
2476
+ color: #9aa5ce;
2477
+ line-height: 1.5;
2478
+ word-break: break-word;
2479
+ }
2480
+ .obs-card-body p { margin: 0 0 6px; white-space: pre-wrap; }
2481
+ .obs-card-facts { display: flex; flex-wrap: wrap; gap: 6px; }
2482
+ .obs-fact {
2483
+ font-size: 10px;
2484
+ color: #565f89;
2485
+ background: #1c1e2b;
2486
+ border: 1px solid #262a3f;
2487
+ border-radius: 5px;
2488
+ padding: 1px 6px;
2489
+ font-variant-numeric: tabular-nums;
2490
+ }
2491
+ .obs-card-caret {
2492
+ position: absolute;
2493
+ right: 10px;
2494
+ top: 10px;
2495
+ font-size: 10px;
2496
+ color: #414868;
2497
+ transition: transform 0.12s ease;
2498
+ }
2499
+ .obs-card-caret.open { transform: rotate(90deg); }
2500
+
2501
+ /* One-line heartbeats (reviews ran, attach/detach) and health warnings */
2502
+ .obs-line {
2503
+ display: flex;
2504
+ align-items: baseline;
2505
+ gap: 8px;
2506
+ padding: 3px 12px 3px 14px;
2507
+ font-size: 11px;
2508
+ color: #565f89;
2509
+ line-height: 1.45;
2510
+ }
2511
+ .obs-line-dot {
2512
+ flex-shrink: 0;
2513
+ width: 4px; height: 4px;
2514
+ border-radius: 50%;
2515
+ background: #3b4261;
2516
+ align-self: center;
2517
+ }
2518
+ .obs-line.health { color: #e0af68; }
2519
+ .obs-line.health .obs-line-dot { background: #e0af68; }
2520
+ .obs-line-text { min-width: 0; word-break: break-word; }
2521
+ .obs-line-time {
2522
+ margin-left: auto;
2523
+ flex-shrink: 0;
2524
+ font-size: 10px;
2525
+ color: #414868;
2526
+ font-variant-numeric: tabular-nums;
2527
+ }
2528
+
2529
+ /* Sensitivity lever + processing indicator row */
2530
+ .obs-toolbar {
2531
+ display: flex;
2532
+ align-items: center;
2533
+ justify-content: space-between;
2534
+ gap: 10px;
2535
+ padding: 8px 14px;
2536
+ border-bottom: 1px solid #23263a;
2537
+ }
2538
+ .obs-sensitivity {
2539
+ display: inline-flex;
2540
+ background: #15161f;
2541
+ border: 1px solid #262a3f;
2542
+ border-radius: 7px;
2543
+ padding: 2px;
2544
+ gap: 2px;
2545
+ }
2546
+ .obs-sens-btn {
2547
+ appearance: none;
2548
+ border: none;
2549
+ background: transparent;
2550
+ color: #565f89;
2551
+ font: inherit;
2552
+ font-size: 10.5px;
2553
+ font-weight: 600;
2554
+ padding: 3px 9px;
2555
+ border-radius: 5px;
2556
+ cursor: pointer;
2557
+ transition: background 0.12s ease, color 0.12s ease;
2558
+ white-space: nowrap;
2559
+ }
2560
+ .obs-sens-btn:hover { color: #9aa5ce; }
2561
+ .obs-sens-btn.active { background: #2a2f4a; color: #c0caf5; }
2562
+
2563
+ /* "reviewing" bouncing dots */
2564
+ .obs-reviewing {
2565
+ display: inline-flex;
2566
+ align-items: center;
2567
+ gap: 3px;
2568
+ font-size: 10.5px;
2569
+ color: #7aa2f7;
2570
+ letter-spacing: 0.03em;
2571
+ }
2572
+ .obs-reviewing-dot {
2573
+ width: 4px; height: 4px;
2574
+ border-radius: 50%;
2575
+ background: #7aa2f7;
2576
+ animation: obs-dot-bounce 1.1s ease-in-out infinite;
2577
+ }
2578
+ .obs-reviewing-dot:nth-child(2) { animation-delay: 0.15s; }
2579
+ .obs-reviewing-dot:nth-child(3) { animation-delay: 0.3s; margin-right: 4px; }
2580
+ @keyframes obs-dot-bounce {
2581
+ 0%, 60%, 100% { transform: translateY(0); opacity: 0.5; }
2582
+ 30% { transform: translateY(-3px); opacity: 1; }
2583
+ }
2584
+
2585
+ /* Pulsing eye badge while a model review is in flight */
2586
+ .obs-badge { position: relative; }
2587
+ .obs-badge.reviewing { color: #7aa2f7; }
2588
+ .obs-badge.reviewing::after {
2589
+ content: '';
2590
+ position: absolute;
2591
+ inset: -1px;
2592
+ border-radius: inherit;
2593
+ border: 1px solid #7aa2f7;
2594
+ animation: obs-badge-pulse 1.6s ease-out infinite;
2595
+ pointer-events: none;
2596
+ }
2597
+ @keyframes obs-badge-pulse {
2598
+ 0% { opacity: 0.9; transform: scale(1); }
2599
+ 70% { opacity: 0; transform: scale(1.35); }
2600
+ 100% { opacity: 0; transform: scale(1.35); }
2601
+ }