@phenx-inc/ctlsurf 0.13.6 → 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 (43) 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 +11665 -286
  4. package/out/main/node-CUnQxFha.js +868 -0
  5. package/out/preload/index.js +17 -0
  6. package/out/renderer/assets/{cssMode-CsSLnKZ-.js → cssMode-7adWSORr.js} +3 -3
  7. package/out/renderer/assets/{freemarker2-C1XYRbOq.js → freemarker2-BkLLWnzk.js} +1 -1
  8. package/out/renderer/assets/{handlebars-BSdbbnMW.js → handlebars-Cinfm9fZ.js} +1 -1
  9. package/out/renderer/assets/{html-96igtyOz.js → html-vl9fkHKL.js} +1 -1
  10. package/out/renderer/assets/{htmlMode-BRnCcIzp.js → htmlMode-BtGqkTNI.js} +3 -3
  11. package/out/renderer/assets/{index-Cc-jDHm3.js → index-BhrV6ld-.js} +359 -23
  12. package/out/renderer/assets/{index-BEekQe3-.css → index-BpCORklm.css} +217 -0
  13. package/out/renderer/assets/{javascript-7MdRNHk9.js → javascript-B7CCdfuW.js} +2 -2
  14. package/out/renderer/assets/{jsonMode-oGam2go_.js → jsonMode-ut9LWBMJ.js} +3 -3
  15. package/out/renderer/assets/{liquid-DoKC7ba2.js → liquid-DVDJ0lKt.js} +1 -1
  16. package/out/renderer/assets/{lspLanguageFeatures-C2h5qGCv.js → lspLanguageFeatures-BbMj8Wsc.js} +1 -1
  17. package/out/renderer/assets/{mdx-0w8n1HGo.js → mdx-CcQzYB0J.js} +1 -1
  18. package/out/renderer/assets/{python-BfY4HssB.js → python-Byy7wKMv.js} +1 -1
  19. package/out/renderer/assets/{razor-BoDk9Ula.js → razor-D1xi_wMM.js} +1 -1
  20. package/out/renderer/assets/{tsMode-CX0oVKRt.js → tsMode-DJPzbn5i.js} +1 -1
  21. package/out/renderer/assets/{typescript-BqIU7abP.js → typescript-BuVaU7i3.js} +1 -1
  22. package/out/renderer/assets/{xml-BlSDnxFw.js → xml-rzc91O3d.js} +1 -1
  23. package/out/renderer/assets/{yaml-bVMmGUtU.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 +36 -2
  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 +36 -0
  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/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>
@@ -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
+ }