@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.
- package/out/headless/index.mjs +13663 -385
- package/out/headless/index.mjs.map +4 -4
- package/out/main/index.js +11677 -290
- package/out/main/node-CUnQxFha.js +868 -0
- package/out/preload/index.js +19 -0
- package/out/renderer/assets/{cssMode-CAEObGNt.js → cssMode-7adWSORr.js} +3 -3
- package/out/renderer/assets/{freemarker2-D63hnnDq.js → freemarker2-BkLLWnzk.js} +1 -1
- package/out/renderer/assets/{handlebars-UYo_IYpi.js → handlebars-Cinfm9fZ.js} +1 -1
- package/out/renderer/assets/{html-2dvYDi3g.js → html-vl9fkHKL.js} +1 -1
- package/out/renderer/assets/{htmlMode-B6fxDDPF.js → htmlMode-BtGqkTNI.js} +3 -3
- package/out/renderer/assets/{index-CMb4AqkZ.js → index-BhrV6ld-.js} +381 -29
- package/out/renderer/assets/{index-BEekQe3-.css → index-BpCORklm.css} +217 -0
- package/out/renderer/assets/{javascript-8RyFDDdk.js → javascript-B7CCdfuW.js} +2 -2
- package/out/renderer/assets/{jsonMode-CqspkWCb.js → jsonMode-ut9LWBMJ.js} +3 -3
- package/out/renderer/assets/{liquid-DaEnAsYY.js → liquid-DVDJ0lKt.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-Czkp5sZb.js → lspLanguageFeatures-BbMj8Wsc.js} +1 -1
- package/out/renderer/assets/{mdx-Q-qSZsFg.js → mdx-CcQzYB0J.js} +1 -1
- package/out/renderer/assets/{python-Sw0S0BJV.js → python-Byy7wKMv.js} +1 -1
- package/out/renderer/assets/{razor-C7zQmabz.js → razor-D1xi_wMM.js} +1 -1
- package/out/renderer/assets/{tsMode-BWdOxk6v.js → tsMode-DJPzbn5i.js} +1 -1
- package/out/renderer/assets/{typescript-BrdIiKZf.js → typescript-BuVaU7i3.js} +1 -1
- package/out/renderer/assets/{xml-DbKLNPOq.js → xml-rzc91O3d.js} +1 -1
- package/out/renderer/assets/{yaml-ehupJBsZ.js → yaml-fAOzQMge.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +2 -1
- package/src/main/index.ts +55 -8
- package/src/main/observer/anthropic.ts +48 -0
- package/src/main/observer/budget.ts +79 -0
- package/src/main/observer/ledger.ts +61 -0
- package/src/main/observer/observer.ts +566 -0
- package/src/main/observer/spans.ts +170 -0
- package/src/main/observer/store.ts +200 -0
- package/src/main/observer/tier0.ts +234 -0
- package/src/main/observer/tier1.ts +306 -0
- package/src/main/observer/tier2.ts +125 -0
- package/src/main/observer/types.ts +224 -0
- package/src/main/orchestrator.ts +74 -0
- package/src/main/transcripts.ts +18 -0
- package/src/preload/index.ts +40 -2
- package/src/renderer/App.tsx +22 -0
- package/src/renderer/components/ObserverPanel.tsx +289 -0
- package/src/renderer/components/SettingsDialog.tsx +104 -0
- package/src/renderer/components/TerminalPanel.tsx +28 -5
- package/src/renderer/styles.css +217 -0
|
@@ -9768,3 +9768,220 @@ html, body, #root {
|
|
|
9768
9768
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
|
9769
9769
|
pointer-events: none;
|
|
9770
9770
|
}
|
|
9771
|
+
|
|
9772
|
+
/* ─── Observer pane ─────────────────────────────── */
|
|
9773
|
+
|
|
9774
|
+
/* Header on/off switch */
|
|
9775
|
+
.observer-switch { display: inline-flex; align-items: center; cursor: pointer; padding-top: 2px; }
|
|
9776
|
+
.observer-switch input { position: absolute; opacity: 0; width: 0; height: 0; }
|
|
9777
|
+
.observer-switch-track {
|
|
9778
|
+
width: 34px; height: 19px; border-radius: 10px;
|
|
9779
|
+
background: #3b4261; position: relative; transition: background 0.15s ease;
|
|
9780
|
+
display: inline-block; flex-shrink: 0;
|
|
9781
|
+
}
|
|
9782
|
+
.observer-switch-thumb {
|
|
9783
|
+
position: absolute; top: 2px; left: 2px; width: 15px; height: 15px;
|
|
9784
|
+
border-radius: 50%; background: #c0caf5; transition: transform 0.15s ease;
|
|
9785
|
+
}
|
|
9786
|
+
.observer-switch input:checked + .observer-switch-track { background: #9ece6a; }
|
|
9787
|
+
.observer-switch input:checked + .observer-switch-track .observer-switch-thumb {
|
|
9788
|
+
transform: translateX(15px); background: #1a1b26;
|
|
9789
|
+
}
|
|
9790
|
+
.observer-switch input:disabled + .observer-switch-track { opacity: 0.5; }
|
|
9791
|
+
|
|
9792
|
+
.obs-feed { gap: 6px; }
|
|
9793
|
+
|
|
9794
|
+
/* Finding / digest cards */
|
|
9795
|
+
.obs-card {
|
|
9796
|
+
position: relative;
|
|
9797
|
+
display: flex;
|
|
9798
|
+
flex-direction: column;
|
|
9799
|
+
gap: 5px;
|
|
9800
|
+
width: 100%;
|
|
9801
|
+
text-align: left;
|
|
9802
|
+
appearance: none;
|
|
9803
|
+
font: inherit;
|
|
9804
|
+
color: inherit;
|
|
9805
|
+
cursor: pointer;
|
|
9806
|
+
background: #15161f;
|
|
9807
|
+
border: 1px solid #262a3f;
|
|
9808
|
+
border-left: 3px solid #565f89;
|
|
9809
|
+
border-radius: 9px;
|
|
9810
|
+
padding: 9px 26px 9px 12px;
|
|
9811
|
+
transition: border-color 0.12s ease, background 0.12s ease;
|
|
9812
|
+
}
|
|
9813
|
+
.obs-card:hover { background: #181a26; border-color: #323752; border-left-width: 3px; }
|
|
9814
|
+
.obs-card:active { transform: scale(0.995); }
|
|
9815
|
+
|
|
9816
|
+
.obs-card-top {
|
|
9817
|
+
display: flex;
|
|
9818
|
+
align-items: center;
|
|
9819
|
+
gap: 8px;
|
|
9820
|
+
min-width: 0;
|
|
9821
|
+
}
|
|
9822
|
+
.obs-chip {
|
|
9823
|
+
display: inline-flex;
|
|
9824
|
+
align-items: center;
|
|
9825
|
+
gap: 5px;
|
|
9826
|
+
flex-shrink: 0;
|
|
9827
|
+
font-size: 9.5px;
|
|
9828
|
+
font-weight: 600;
|
|
9829
|
+
letter-spacing: 0.06em;
|
|
9830
|
+
text-transform: uppercase;
|
|
9831
|
+
padding: 2px 7px 2px 6px;
|
|
9832
|
+
border: 1px solid;
|
|
9833
|
+
border-radius: 999px;
|
|
9834
|
+
line-height: 1.4;
|
|
9835
|
+
}
|
|
9836
|
+
.obs-chip-dot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }
|
|
9837
|
+
.obs-card-cat {
|
|
9838
|
+
font-size: 10px;
|
|
9839
|
+
color: #565f89;
|
|
9840
|
+
letter-spacing: 0.05em;
|
|
9841
|
+
text-transform: uppercase;
|
|
9842
|
+
white-space: nowrap;
|
|
9843
|
+
overflow: hidden;
|
|
9844
|
+
text-overflow: ellipsis;
|
|
9845
|
+
}
|
|
9846
|
+
.obs-card-time {
|
|
9847
|
+
margin-left: auto;
|
|
9848
|
+
flex-shrink: 0;
|
|
9849
|
+
font-size: 10px;
|
|
9850
|
+
color: #414868;
|
|
9851
|
+
font-variant-numeric: tabular-nums;
|
|
9852
|
+
}
|
|
9853
|
+
.obs-card-title {
|
|
9854
|
+
font-size: 12.5px;
|
|
9855
|
+
font-weight: 600;
|
|
9856
|
+
color: #c0caf5;
|
|
9857
|
+
line-height: 1.4;
|
|
9858
|
+
word-break: break-word;
|
|
9859
|
+
}
|
|
9860
|
+
.obs-card-body {
|
|
9861
|
+
font-size: 11.5px;
|
|
9862
|
+
color: #9aa5ce;
|
|
9863
|
+
line-height: 1.5;
|
|
9864
|
+
word-break: break-word;
|
|
9865
|
+
}
|
|
9866
|
+
.obs-card-body p { margin: 0 0 6px; white-space: pre-wrap; }
|
|
9867
|
+
.obs-card-facts { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
9868
|
+
.obs-fact {
|
|
9869
|
+
font-size: 10px;
|
|
9870
|
+
color: #565f89;
|
|
9871
|
+
background: #1c1e2b;
|
|
9872
|
+
border: 1px solid #262a3f;
|
|
9873
|
+
border-radius: 5px;
|
|
9874
|
+
padding: 1px 6px;
|
|
9875
|
+
font-variant-numeric: tabular-nums;
|
|
9876
|
+
}
|
|
9877
|
+
.obs-card-caret {
|
|
9878
|
+
position: absolute;
|
|
9879
|
+
right: 10px;
|
|
9880
|
+
top: 10px;
|
|
9881
|
+
font-size: 10px;
|
|
9882
|
+
color: #414868;
|
|
9883
|
+
transition: transform 0.12s ease;
|
|
9884
|
+
}
|
|
9885
|
+
.obs-card-caret.open { transform: rotate(90deg); }
|
|
9886
|
+
|
|
9887
|
+
/* One-line heartbeats (reviews ran, attach/detach) and health warnings */
|
|
9888
|
+
.obs-line {
|
|
9889
|
+
display: flex;
|
|
9890
|
+
align-items: baseline;
|
|
9891
|
+
gap: 8px;
|
|
9892
|
+
padding: 3px 12px 3px 14px;
|
|
9893
|
+
font-size: 11px;
|
|
9894
|
+
color: #565f89;
|
|
9895
|
+
line-height: 1.45;
|
|
9896
|
+
}
|
|
9897
|
+
.obs-line-dot {
|
|
9898
|
+
flex-shrink: 0;
|
|
9899
|
+
width: 4px; height: 4px;
|
|
9900
|
+
border-radius: 50%;
|
|
9901
|
+
background: #3b4261;
|
|
9902
|
+
align-self: center;
|
|
9903
|
+
}
|
|
9904
|
+
.obs-line.health { color: #e0af68; }
|
|
9905
|
+
.obs-line.health .obs-line-dot { background: #e0af68; }
|
|
9906
|
+
.obs-line-text { min-width: 0; word-break: break-word; }
|
|
9907
|
+
.obs-line-time {
|
|
9908
|
+
margin-left: auto;
|
|
9909
|
+
flex-shrink: 0;
|
|
9910
|
+
font-size: 10px;
|
|
9911
|
+
color: #414868;
|
|
9912
|
+
font-variant-numeric: tabular-nums;
|
|
9913
|
+
}
|
|
9914
|
+
|
|
9915
|
+
/* Sensitivity lever + processing indicator row */
|
|
9916
|
+
.obs-toolbar {
|
|
9917
|
+
display: flex;
|
|
9918
|
+
align-items: center;
|
|
9919
|
+
justify-content: space-between;
|
|
9920
|
+
gap: 10px;
|
|
9921
|
+
padding: 8px 14px;
|
|
9922
|
+
border-bottom: 1px solid #23263a;
|
|
9923
|
+
}
|
|
9924
|
+
.obs-sensitivity {
|
|
9925
|
+
display: inline-flex;
|
|
9926
|
+
background: #15161f;
|
|
9927
|
+
border: 1px solid #262a3f;
|
|
9928
|
+
border-radius: 7px;
|
|
9929
|
+
padding: 2px;
|
|
9930
|
+
gap: 2px;
|
|
9931
|
+
}
|
|
9932
|
+
.obs-sens-btn {
|
|
9933
|
+
appearance: none;
|
|
9934
|
+
border: none;
|
|
9935
|
+
background: transparent;
|
|
9936
|
+
color: #565f89;
|
|
9937
|
+
font: inherit;
|
|
9938
|
+
font-size: 10.5px;
|
|
9939
|
+
font-weight: 600;
|
|
9940
|
+
padding: 3px 9px;
|
|
9941
|
+
border-radius: 5px;
|
|
9942
|
+
cursor: pointer;
|
|
9943
|
+
transition: background 0.12s ease, color 0.12s ease;
|
|
9944
|
+
white-space: nowrap;
|
|
9945
|
+
}
|
|
9946
|
+
.obs-sens-btn:hover { color: #9aa5ce; }
|
|
9947
|
+
.obs-sens-btn.active { background: #2a2f4a; color: #c0caf5; }
|
|
9948
|
+
|
|
9949
|
+
/* "reviewing" bouncing dots */
|
|
9950
|
+
.obs-reviewing {
|
|
9951
|
+
display: inline-flex;
|
|
9952
|
+
align-items: center;
|
|
9953
|
+
gap: 3px;
|
|
9954
|
+
font-size: 10.5px;
|
|
9955
|
+
color: #7aa2f7;
|
|
9956
|
+
letter-spacing: 0.03em;
|
|
9957
|
+
}
|
|
9958
|
+
.obs-reviewing-dot {
|
|
9959
|
+
width: 4px; height: 4px;
|
|
9960
|
+
border-radius: 50%;
|
|
9961
|
+
background: #7aa2f7;
|
|
9962
|
+
animation: obs-dot-bounce 1.1s ease-in-out infinite;
|
|
9963
|
+
}
|
|
9964
|
+
.obs-reviewing-dot:nth-child(2) { animation-delay: 0.15s; }
|
|
9965
|
+
.obs-reviewing-dot:nth-child(3) { animation-delay: 0.3s; margin-right: 4px; }
|
|
9966
|
+
@keyframes obs-dot-bounce {
|
|
9967
|
+
0%, 60%, 100% { transform: translateY(0); opacity: 0.5; }
|
|
9968
|
+
30% { transform: translateY(-3px); opacity: 1; }
|
|
9969
|
+
}
|
|
9970
|
+
|
|
9971
|
+
/* Pulsing eye badge while a model review is in flight */
|
|
9972
|
+
.obs-badge { position: relative; }
|
|
9973
|
+
.obs-badge.reviewing { color: #7aa2f7; }
|
|
9974
|
+
.obs-badge.reviewing::after {
|
|
9975
|
+
content: '';
|
|
9976
|
+
position: absolute;
|
|
9977
|
+
inset: -1px;
|
|
9978
|
+
border-radius: inherit;
|
|
9979
|
+
border: 1px solid #7aa2f7;
|
|
9980
|
+
animation: obs-badge-pulse 1.6s ease-out infinite;
|
|
9981
|
+
pointer-events: none;
|
|
9982
|
+
}
|
|
9983
|
+
@keyframes obs-badge-pulse {
|
|
9984
|
+
0% { opacity: 0.9; transform: scale(1); }
|
|
9985
|
+
70% { opacity: 0; transform: scale(1.35); }
|
|
9986
|
+
100% { opacity: 0; transform: scale(1.35); }
|
|
9987
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { conf as conf$1, language as language$1 } from "./typescript-
|
|
2
|
-
import "./index-
|
|
1
|
+
import { conf as conf$1, language as language$1 } from "./typescript-BuVaU7i3.js";
|
|
2
|
+
import "./index-BhrV6ld-.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-
|
|
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-
|
|
3
|
-
import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-
|
|
1
|
+
import { c as createWebWorker, l as languages, e as editor } from "./index-BhrV6ld-.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-BbMj8Wsc.js";
|
|
3
|
+
import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-BbMj8Wsc.js";
|
|
4
4
|
const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
|
|
5
5
|
class WorkerManager {
|
|
6
6
|
constructor(defaults) {
|
package/out/renderer/assets/{lspLanguageFeatures-Czkp5sZb.js → lspLanguageFeatures-BbMj8Wsc.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index-
|
|
1
|
+
import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index-BhrV6ld-.js";
|
|
2
2
|
var DocumentUri;
|
|
3
3
|
(function(DocumentUri2) {
|
|
4
4
|
function is(value) {
|
|
@@ -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-
|
|
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-BhrV6ld-.js";
|
|
2
2
|
class WorkerManager {
|
|
3
3
|
constructor(_modeId, _defaults) {
|
|
4
4
|
this._modeId = _modeId;
|
package/out/renderer/index.html
CHANGED
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
color: #565f89;
|
|
50
50
|
}
|
|
51
51
|
</style>
|
|
52
|
-
<script type="module" crossorigin src="./assets/index-
|
|
53
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
52
|
+
<script type="module" crossorigin src="./assets/index-BhrV6ld-.js"></script>
|
|
53
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BpCORklm.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.
|
|
3
|
+
"version": "0.14.0",
|
|
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": {
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"node": ">=18"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
+
"@anthropic-ai/sdk": "^0.110.0",
|
|
46
47
|
"@huggingface/transformers": "^4.2.0",
|
|
47
48
|
"@monaco-editor/react": "^4.7.0",
|
|
48
49
|
"@xterm/addon-fit": "^0.10.0",
|
package/src/main/index.ts
CHANGED
|
@@ -213,6 +213,8 @@ const orchestrator = new Orchestrator(
|
|
|
213
213
|
onWorkerRegistered: (data) => mainWindow?.webContents.send('worker:registered', data),
|
|
214
214
|
onProjectChanged: (name) => mainWindow?.webContents.send('app:projectChanged', name),
|
|
215
215
|
onAgentMessage: (text) => mainWindow?.webContents.send('agent:message', text),
|
|
216
|
+
onObserverUpdate: (item) => mainWindow?.webContents.send('observer:update', item),
|
|
217
|
+
onObserverActivity: (reviewing) => mainWindow?.webContents.send('observer:activity', reviewing),
|
|
216
218
|
onAgentStateChange: (tabId, state, anyAwaiting) => {
|
|
217
219
|
log(`[agent-state] tab=${tabId} state=${state} anyAwaiting=${anyAwaiting}`)
|
|
218
220
|
mainWindow?.webContents.send('agent:state', tabId, state, anyAwaiting)
|
|
@@ -283,16 +285,29 @@ function createWindow(): void {
|
|
|
283
285
|
// ─── IPC Handlers ──────────────────────────────────
|
|
284
286
|
|
|
285
287
|
// Write a rich-HTML table (plus plain-text fallback) to the clipboard so it
|
|
286
|
-
// can be pasted as a formatted table into an email client.
|
|
288
|
+
// can be pasted as a formatted table into an email client. Verify the write
|
|
289
|
+
// landed by reading the plain-text side back — clipboard.write never throws on
|
|
290
|
+
// macOS even when the pasteboard write is dropped, so a read-back is the only
|
|
291
|
+
// reliable success signal.
|
|
287
292
|
ipcMain.handle('clipboard:writeTable', (_event, html: string, text: string) => {
|
|
288
|
-
|
|
289
|
-
|
|
293
|
+
try {
|
|
294
|
+
clipboard.write({ html, text })
|
|
295
|
+
return { ok: clipboard.readText() === text }
|
|
296
|
+
} catch (err) {
|
|
297
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) }
|
|
298
|
+
}
|
|
290
299
|
})
|
|
291
300
|
|
|
292
|
-
// Plain-text clipboard write, for the terminal right-click "Copy" item
|
|
301
|
+
// Plain-text clipboard write, for the terminal right-click "Copy" item and the
|
|
302
|
+
// snapshot panel. Read the value back and report whether it actually stuck, so
|
|
303
|
+
// the renderer can show an honest toast instead of claiming success blindly.
|
|
293
304
|
ipcMain.handle('clipboard:writeText', (_event, text: string) => {
|
|
294
|
-
|
|
295
|
-
|
|
305
|
+
try {
|
|
306
|
+
clipboard.writeText(text)
|
|
307
|
+
return { ok: clipboard.readText() === text }
|
|
308
|
+
} catch (err) {
|
|
309
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) }
|
|
310
|
+
}
|
|
296
311
|
})
|
|
297
312
|
|
|
298
313
|
ipcMain.handle('pty:spawn', async (_event, tabId: string, agent: AgentConfig, cwd: string) => {
|
|
@@ -606,6 +621,19 @@ ipcMain.handle('speak:set', (_event, enabled: boolean) => {
|
|
|
606
621
|
orchestrator.setSpeakReplies(!!enabled)
|
|
607
622
|
return { enabled: orchestrator.speakRepliesEnabled }
|
|
608
623
|
})
|
|
624
|
+
|
|
625
|
+
// ─── Observer IPC ─────────────────────────────────
|
|
626
|
+
|
|
627
|
+
ipcMain.handle('observer:get', () => orchestrator.getObserverSettingsSummary())
|
|
628
|
+
ipcMain.handle('observer:set', (_event, data: { enabled: boolean; anthropicApiKey?: string; dailyCapUsd?: number; sensitivity?: 'severe' | 'balanced' | 'all' }) => {
|
|
629
|
+
orchestrator.saveObserverSettings(data)
|
|
630
|
+
return orchestrator.getObserverSettingsSummary()
|
|
631
|
+
})
|
|
632
|
+
ipcMain.handle('observer:feed', () => orchestrator.observer.getFeed())
|
|
633
|
+
ipcMain.handle('observer:toggle', (_event, enabled: boolean) => {
|
|
634
|
+
orchestrator.setObserverEnabled(!!enabled)
|
|
635
|
+
return orchestrator.observer.getFeed()
|
|
636
|
+
})
|
|
609
637
|
// Latest assistant reply from the active tab's transcript, for on-demand TTS.
|
|
610
638
|
ipcMain.handle('agent:getLastReply', () => orchestrator.getLastReply())
|
|
611
639
|
|
|
@@ -696,9 +724,28 @@ app.whenReady().then(() => {
|
|
|
696
724
|
setInterval(() => { void checkForUpdate() }, UPDATE_CHECK_INTERVAL_MS)
|
|
697
725
|
})
|
|
698
726
|
|
|
727
|
+
// Shutdown is async (final Tier-1 flush, session digest, time-tracker
|
|
728
|
+
// writes). Hold the quit until it settles — capped so a hung network call
|
|
729
|
+
// can never trap the process — otherwise that work is lost. before-quit
|
|
730
|
+
// covers Cmd+Q; window-all-closed covers closing the last window.
|
|
731
|
+
let shutdownStarted = false
|
|
732
|
+
function shutdownThenQuit(): void {
|
|
733
|
+
if (shutdownStarted) return
|
|
734
|
+
shutdownStarted = true
|
|
735
|
+
void Promise.race([
|
|
736
|
+
orchestrator.shutdown(),
|
|
737
|
+
new Promise((resolve) => setTimeout(resolve, 10_000)),
|
|
738
|
+
]).finally(() => app.quit())
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
app.on('before-quit', (event) => {
|
|
742
|
+
if (shutdownStarted) return // second pass: let the quit proceed
|
|
743
|
+
event.preventDefault()
|
|
744
|
+
shutdownThenQuit()
|
|
745
|
+
})
|
|
746
|
+
|
|
699
747
|
app.on('window-all-closed', () => {
|
|
700
|
-
|
|
701
|
-
app.quit()
|
|
748
|
+
shutdownThenQuit()
|
|
702
749
|
})
|
|
703
750
|
|
|
704
751
|
app.on('activate', () => {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import Anthropic from '@anthropic-ai/sdk'
|
|
2
|
+
import type { ModelCaller, ModelCallRequest, ModelCallResult } from './types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Production ModelCaller backed by @anthropic-ai/sdk.
|
|
6
|
+
*
|
|
7
|
+
* The system block carries a cache_control marker so the 5-min prompt cache
|
|
8
|
+
* is refreshed by the Tier-1 call cadence (spec §3.2). Structured output
|
|
9
|
+
* (output_config.format json_schema) enforces the strict-JSON contract when
|
|
10
|
+
* a schema is provided.
|
|
11
|
+
*/
|
|
12
|
+
export function createModelCaller(apiKey: string): ModelCaller {
|
|
13
|
+
const client = new Anthropic({ apiKey })
|
|
14
|
+
|
|
15
|
+
return async (req: ModelCallRequest): Promise<ModelCallResult> => {
|
|
16
|
+
const params: Record<string, unknown> = {
|
|
17
|
+
model: req.model,
|
|
18
|
+
max_tokens: req.maxTokens,
|
|
19
|
+
system: [
|
|
20
|
+
{ type: 'text', text: req.system, cache_control: { type: 'ephemeral' } },
|
|
21
|
+
],
|
|
22
|
+
messages: [{ role: 'user', content: req.user }],
|
|
23
|
+
}
|
|
24
|
+
if (req.schema) {
|
|
25
|
+
params.output_config = { format: { type: 'json_schema', schema: req.schema } }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The SDK's create() is typed narrowly around output_config across
|
|
29
|
+
// versions; pass through as any to keep the wire shape authoritative.
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
+
const response: any = await (client.messages.create as any)(params)
|
|
32
|
+
|
|
33
|
+
const text = (response.content || [])
|
|
34
|
+
.filter((b: any) => b?.type === 'text' && typeof b.text === 'string')
|
|
35
|
+
.map((b: any) => b.text)
|
|
36
|
+
.join('')
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
text,
|
|
40
|
+
usage: {
|
|
41
|
+
inputTokens: response.usage?.input_tokens ?? 0,
|
|
42
|
+
outputTokens: response.usage?.output_tokens ?? 0,
|
|
43
|
+
cacheReadTokens: response.usage?.cache_read_input_tokens ?? 0,
|
|
44
|
+
cacheWriteTokens: response.usage?.cache_creation_input_tokens ?? 0,
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Daily spend guard (spec §9). Tracks approximate model cost in a small
|
|
6
|
+
* JSON file; on cap breach Tier 1 degrades to Tier-0-tagged events only
|
|
7
|
+
* and an observer.health event is emitted by the caller.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
interface Usage {
|
|
11
|
+
inputTokens: number
|
|
12
|
+
outputTokens: number
|
|
13
|
+
cacheReadTokens: number
|
|
14
|
+
cacheWriteTokens: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** $/MTok — input, output. Cache reads 0.1x input, writes 1.25x input. */
|
|
18
|
+
const PRICES: Record<string, { in: number; out: number }> = {
|
|
19
|
+
'claude-haiku-4-5': { in: 1, out: 5 },
|
|
20
|
+
'claude-sonnet-4-6': { in: 3, out: 15 },
|
|
21
|
+
'claude-sonnet-5': { in: 3, out: 15 },
|
|
22
|
+
}
|
|
23
|
+
const DEFAULT_PRICE = { in: 3, out: 15 }
|
|
24
|
+
|
|
25
|
+
export function estimateCostUsd(model: string, usage: Usage): number {
|
|
26
|
+
const p = PRICES[model] || DEFAULT_PRICE
|
|
27
|
+
return (
|
|
28
|
+
(usage.inputTokens * p.in +
|
|
29
|
+
usage.outputTokens * p.out +
|
|
30
|
+
usage.cacheReadTokens * p.in * 0.1 +
|
|
31
|
+
usage.cacheWriteTokens * p.in * 1.25) /
|
|
32
|
+
1_000_000
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class ObserverBudget {
|
|
37
|
+
private readonly filePath: string
|
|
38
|
+
private readonly capUsd: number
|
|
39
|
+
private date: string
|
|
40
|
+
private spentUsd = 0
|
|
41
|
+
|
|
42
|
+
constructor(dir: string, capUsd: number, now: () => Date = () => new Date()) {
|
|
43
|
+
this.filePath = path.join(dir, 'budget.json')
|
|
44
|
+
this.capUsd = capUsd
|
|
45
|
+
this.date = localDay(now())
|
|
46
|
+
try {
|
|
47
|
+
const raw = JSON.parse(fs.readFileSync(this.filePath, 'utf-8'))
|
|
48
|
+
if (raw?.date === this.date && typeof raw.spentUsd === 'number') {
|
|
49
|
+
this.spentUsd = raw.spentUsd
|
|
50
|
+
}
|
|
51
|
+
} catch { /* fresh day / first run */ }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
addUsage(model: string, usage: Usage, now: () => Date = () => new Date()): void {
|
|
55
|
+
const today = localDay(now())
|
|
56
|
+
if (today !== this.date) {
|
|
57
|
+
this.date = today
|
|
58
|
+
this.spentUsd = 0
|
|
59
|
+
}
|
|
60
|
+
this.spentUsd += estimateCostUsd(model, usage)
|
|
61
|
+
try {
|
|
62
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true })
|
|
63
|
+
fs.writeFileSync(this.filePath, JSON.stringify({ date: this.date, spentUsd: this.spentUsd }))
|
|
64
|
+
} catch { /* best-effort */ }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
overCap(now: () => Date = () => new Date()): boolean {
|
|
68
|
+
if (localDay(now()) !== this.date) return false // new day resets on next addUsage
|
|
69
|
+
return this.spentUsd >= this.capUsd
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get spent(): number {
|
|
73
|
+
return this.spentUsd
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function localDay(d: Date): string {
|
|
78
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
79
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import type { LedgerEvent, LedgerEventType } from './types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Session Ledger
|
|
7
|
+
*
|
|
8
|
+
* Append-only JSONL file per observed session — the substrate everything
|
|
9
|
+
* else consumes (spec §1, §6). Events get monotonic `evt_N` ids; Observer
|
|
10
|
+
* outputs (findings, spans, digests, health) are appended to the same file
|
|
11
|
+
* with their own id prefixes so receipts always resolve within the file.
|
|
12
|
+
*
|
|
13
|
+
* A bounded in-memory tail is kept for ground-truth lookback (e.g. "was a
|
|
14
|
+
* test command actually run since the last user message?").
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const MEMORY_TAIL_EVENTS = 500
|
|
18
|
+
|
|
19
|
+
export class SessionLedger {
|
|
20
|
+
readonly sessionId: string
|
|
21
|
+
readonly filePath: string
|
|
22
|
+
|
|
23
|
+
private seq = 0
|
|
24
|
+
private tail: LedgerEvent[] = []
|
|
25
|
+
|
|
26
|
+
constructor(dir: string, sessionId: string) {
|
|
27
|
+
this.sessionId = sessionId
|
|
28
|
+
this.filePath = path.join(dir, `${sessionId}.jsonl`)
|
|
29
|
+
try { fs.mkdirSync(dir, { recursive: true }) } catch { /* ignore */ }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Append a normalized event; assigns id + session and persists it. */
|
|
33
|
+
append(partial: Omit<LedgerEvent, 'id' | 'session'> & { id?: string }): LedgerEvent {
|
|
34
|
+
const event: LedgerEvent = {
|
|
35
|
+
...partial,
|
|
36
|
+
id: partial.id || this.nextId(partial.type),
|
|
37
|
+
session: this.sessionId,
|
|
38
|
+
}
|
|
39
|
+
this.tail.push(event)
|
|
40
|
+
if (this.tail.length > MEMORY_TAIL_EVENTS) {
|
|
41
|
+
this.tail.splice(0, this.tail.length - MEMORY_TAIL_EVENTS)
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
fs.appendFileSync(this.filePath, JSON.stringify(event) + '\n')
|
|
45
|
+
} catch { /* best-effort: observation must never break the session */ }
|
|
46
|
+
return event
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Recent events, oldest first (bounded to the in-memory tail). */
|
|
50
|
+
recent(limit = MEMORY_TAIL_EVENTS): LedgerEvent[] {
|
|
51
|
+
return limit >= this.tail.length ? [...this.tail] : this.tail.slice(-limit)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private nextId(type: LedgerEventType): string {
|
|
55
|
+
if (type === 'observer.finding') return `fnd_${this.sessionId.slice(4, 10)}_${++this.seq}`
|
|
56
|
+
if (type === 'observer.span') return `spn_${this.sessionId.slice(4, 10)}_${++this.seq}`
|
|
57
|
+
if (type === 'observer.digest') return `dig_${this.sessionId.slice(4, 10)}_${++this.seq}`
|
|
58
|
+
if (type === 'observer.health') return `hlt_${this.sessionId.slice(4, 10)}_${++this.seq}`
|
|
59
|
+
return `evt_${++this.seq}`
|
|
60
|
+
}
|
|
61
|
+
}
|