@lelouchhe/webagent 0.8.0 → 0.9.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/README.md +4 -1
- package/dist/index.html +20 -4
- package/dist/js/app.QC7IRDTP.js +5 -0
- package/dist/js/chunk.3CLGCUHW.js +1 -0
- package/dist/js/chunk.AOTG3PL7.js +20 -0
- package/dist/js/{chunk.CT5WBNGZ.js → chunk.S5LRNRJI.js} +50 -49
- package/dist/js/{login.2WA6DTGM.js → login.WMURU4NI.js} +1 -1
- package/dist/js/{viewer.CVWXSKJM.js → viewer.GP5VXAUY.js} +1 -1
- package/dist/login.html +2 -2
- package/dist/share-viewer.html +4 -4
- package/dist/{styles.00etlpgs.css → styles.00nlhhf3.css} +167 -2
- package/lib/agent-key.js +6 -0
- package/lib/attachments.js +44 -7
- package/lib/auth-middleware.js +9 -2
- package/lib/bridge.js +115 -22
- package/lib/event-handler.js +31 -3
- package/lib/files/limits.js +15 -0
- package/lib/files/paths.js +155 -0
- package/lib/files/routes.js +232 -0
- package/lib/home-path.js +35 -0
- package/lib/http-status.js +1 -0
- package/lib/routes.js +117 -28
- package/lib/server.js +22 -20
- package/lib/session-manager.js +54 -29
- package/lib/session-state.js +29 -0
- package/lib/share/routes.js +12 -0
- package/lib/store.js +99 -18
- package/lib/title-service.js +3 -3
- package/package.json +2 -1
- package/dist/js/app.XBFXH37R.js +0 -2
- package/dist/js/chunk.UMQMOGWO.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{b as r,c as i}from"./chunk.
|
|
1
|
+
import{b as r,c as i}from"./chunk.3CLGCUHW.js";import"./chunk.AJZBJBMO.js";localStorage.getItem(r)&&location.replace("/");var s=document.getElementById("login-form"),n=document.getElementById("token-input"),o=document.getElementById("error"),t=document.getElementById("submit-btn");function u(e){o&&(o.textContent=e,o.style.display=e?"block":"none")}s&&n&&t&&(s.addEventListener("submit",e=>{e.preventDefault(),u(""),t.disabled=!0,t.textContent="Verifying\u2026",(async()=>{try{let l=await i(n.value);l.ok?location.replace("/"):u(l.error)}finally{t.disabled=!1,t.textContent="Sign in"}})()}),n.focus());
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{c as g,e as h,n as y,p as T,q as C}from"./chunk.S5LRNRJI.js";import{a as p}from"./chunk.3CLGCUHW.js";import"./chunk.AJZBJBMO.js";var $=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function f(t){return String(t).padStart(2,"0")}function E(t,n){if(!t)return"";let s=new Date(t),i=s.getTime();if(isNaN(i))return"";let l=Math.floor((n.getTime()-i)/1e3);if(l<60)return"just now";let c=Math.floor(l/60);if(c<60)return`${c}m ago`;let o=Math.floor(c/60);if(o<24)return`${o}h ago`;let e=Math.floor(o/24);if(e<7)return`${e}d ago`;let m=s.getUTCFullYear()===n.getUTCFullYear(),r=$[s.getUTCMonth()],a=s.getUTCDate();return m?`${r} ${a}`:`${r} ${a}, ${s.getUTCFullYear()}`}function _(t){if(!t)return"";let n=new Date(t);return isNaN(n.getTime())?"":`${n.getUTCFullYear()}-${f(n.getUTCMonth()+1)}-${f(n.getUTCDate())} ${f(n.getUTCHours())}:${f(n.getUTCMinutes())} UTC`}function S(t){let n=/^\/api\/v1\/sessions\/[^/]+\/attachments\/([A-Za-z0-9._-]+)(?:\?.*)?$/;return s=>{let i=n.exec(s);return i?`/s/${encodeURIComponent(t)}/attachments/${i[1]}`:s}}function w(t){if(typeof t.data=="string")try{return JSON.parse(t.data)}catch{return{}}return t.data}function x(t,n,s,i){if(t.type!=="assistant_message"||n?.type!=="assistant_message"||t.seq!==n.seq+1||!s?.classList.contains("assistant"))return!1;let l=(s.getAttribute("data-raw")??"")+(typeof i.text=="string"?i.text:"");return s.setAttribute("data-raw",l),C(s,l,void 0,!0),g(s),!0}function v(t,n,s){let i=new Set;for(let r of t)if(r.type==="permission_response")try{let a=JSON.parse(r.data);typeof a.requestId=="string"&&i.add(a.requestId)}catch{}let l=new Map,c=new Map,o=null,e=null,m={rewriteAttachmentSrc:S(s),enhanceMarkdown:g,findToolCallEl:r=>l.get(r)??null,findPermissionEl:r=>c.get(r)??null,findBashEl:()=>o,isPermissionResolved:r=>i.has(r),finalAnswerToolText:null};for(let r=0;r<t.length;r++){let a=t[r];if(!y(a.type))continue;let u=w(a),A=r>0?t[r-1]:null,M=n.lastElementChild;if(x(a,A,M,u)){e=null;continue}m.finalAnswerToolText=a.type==="assistant_message"?e:null;let d=T(a.type,u,m);d?(n.appendChild(d),a.type==="tool_call"&&typeof u.id=="string"?l.set(u.id,d):a.type==="permission_request"&&typeof u.requestId=="string"?c.set(u.requestId,d):a.type==="bash_command"&&(o=d)):a.type==="bash_result"&&(o=null),a.type==="tool_call_update"?e=h(typeof u.status=="string"?u.status:"",Array.isArray(u.content)?u.content:void 0):e=null}}async function U(){try{let e=localStorage.getItem("theme")??"auto";document.documentElement.setAttribute("data-theme",e)}catch{}let t=/^\/s\/([A-Za-z0-9_-]{24})(?:[/?#]|$)/.exec(location.pathname);if(!t){document.body.textContent="invalid share URL";return}let n=t[1],s=document.getElementById("session-info"),i=document.getElementById("messages"),l=document.querySelector(".share-footer-author"),c=document.querySelector(".share-footer-meta");if(!i)return;let o;try{let e=await fetch(`/api/v1/shared/${encodeURIComponent(n)}/events`,{headers:{Accept:"application/json"},credentials:"omit"});if(e.status===p.GONE){document.body.innerHTML="<div class='share-gone'>\u6B64\u94FE\u63A5\u5DF2\u64A4\u9500\u6216\u8FC7\u671F\u3002</div>";return}if(!e.ok){document.body.textContent=`error ${e.status}`;return}o=await e.json()}catch(e){document.body.textContent=`failed to load: ${String(e)}`;return}if(document.title=o.share.session_title?`${o.share.session_title} \u2014 shared`:"shared session",s&&(s.textContent=o.share.session_title??"(untitled)"),l){let e=o.share.display_name??"";l.textContent=e?`by ${e}`:""}if(v(o.events,i,n),c){let e=o.share.shared_at??o.share.created_at;c.textContent=E(e,new Date),c instanceof HTMLElement&&(c.title=_(e))}}U();
|
package/dist/login.html
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
media="(prefers-color-scheme: light)"
|
|
18
18
|
/>
|
|
19
19
|
<script src="/theme-init.js"></script>
|
|
20
|
-
<link rel="stylesheet" href="/styles.
|
|
20
|
+
<link rel="stylesheet" href="/styles.00nlhhf3.css" />
|
|
21
21
|
</head>
|
|
22
22
|
<body class="login-page">
|
|
23
23
|
<div class="login-card">
|
|
@@ -44,6 +44,6 @@
|
|
|
44
44
|
<code>webagent --create-token <name></code> on the server.
|
|
45
45
|
</p>
|
|
46
46
|
</div>
|
|
47
|
-
<script type="module" src="/js/login.
|
|
47
|
+
<script type="module" src="/js/login.WMURU4NI.js"></script>
|
|
48
48
|
</body>
|
|
49
49
|
</html>
|
package/dist/share-viewer.html
CHANGED
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
media="(prefers-color-scheme: light)"
|
|
17
17
|
/>
|
|
18
18
|
<meta name="robots" content="noindex, nofollow" />
|
|
19
|
-
<link rel="stylesheet" href="/s/_/styles.
|
|
19
|
+
<link rel="stylesheet" href="/s/_/styles.00nlhhf3.css" />
|
|
20
20
|
<link rel="stylesheet" href="/s/_/share-viewer.00gubshk.css" />
|
|
21
|
-
<link rel="modulepreload" href="/s/_/chunk.
|
|
22
|
-
<link rel="modulepreload" href="/s/_/chunk.
|
|
21
|
+
<link rel="modulepreload" href="/s/_/chunk.S5LRNRJI.js">
|
|
22
|
+
<link rel="modulepreload" href="/s/_/chunk.3CLGCUHW.js">
|
|
23
23
|
<link rel="modulepreload" href="/s/_/chunk.AJZBJBMO.js">
|
|
24
24
|
</head>
|
|
25
25
|
<body class="share-viewer">
|
|
@@ -49,6 +49,6 @@
|
|
|
49
49
|
>
|
|
50
50
|
</div>
|
|
51
51
|
</footer>
|
|
52
|
-
<script type="module" src="/s/_/viewer.
|
|
52
|
+
<script type="module" src="/s/_/viewer.GP5VXAUY.js"></script>
|
|
53
53
|
</body>
|
|
54
54
|
</html>
|
|
@@ -165,7 +165,8 @@ body {
|
|
|
165
165
|
width: 100%;
|
|
166
166
|
text-align: center;
|
|
167
167
|
}
|
|
168
|
-
#theme-btn
|
|
168
|
+
#theme-btn,
|
|
169
|
+
#file-viewer-close {
|
|
169
170
|
background: transparent;
|
|
170
171
|
border: none;
|
|
171
172
|
color: var(--text-dim);
|
|
@@ -174,7 +175,8 @@ body {
|
|
|
174
175
|
font-family: inherit;
|
|
175
176
|
padding: 2px 6px;
|
|
176
177
|
}
|
|
177
|
-
#theme-btn:hover
|
|
178
|
+
#theme-btn:hover,
|
|
179
|
+
#file-viewer-close:hover {
|
|
178
180
|
color: var(--text);
|
|
179
181
|
}
|
|
180
182
|
@media (max-width: 640px) {
|
|
@@ -748,7 +750,25 @@ body {
|
|
|
748
750
|
#status-bar::before {
|
|
749
751
|
content: "\200b";
|
|
750
752
|
}
|
|
753
|
+
#status-bar .status-model {
|
|
754
|
+
flex: 0 0 auto;
|
|
755
|
+
}
|
|
756
|
+
#status-bar .status-separator,
|
|
757
|
+
#status-bar .status-usage {
|
|
758
|
+
flex: 0 0 auto;
|
|
759
|
+
}
|
|
751
760
|
#status-bar .status-cwd {
|
|
761
|
+
display: flex;
|
|
762
|
+
flex: 1 1 auto;
|
|
763
|
+
min-width: 0;
|
|
764
|
+
overflow: hidden;
|
|
765
|
+
}
|
|
766
|
+
#status-bar .status-cwd-prefix {
|
|
767
|
+
flex: 0 0 auto;
|
|
768
|
+
}
|
|
769
|
+
#status-bar .status-cwd-tail {
|
|
770
|
+
flex: 1 1 auto;
|
|
771
|
+
min-width: 0;
|
|
752
772
|
overflow: hidden;
|
|
753
773
|
text-overflow: ellipsis;
|
|
754
774
|
direction: rtl;
|
|
@@ -990,6 +1010,151 @@ body {
|
|
|
990
1010
|
cursor: default;
|
|
991
1011
|
}
|
|
992
1012
|
|
|
1013
|
+
/* Read-only file viewer */
|
|
1014
|
+
#file-viewer[hidden] {
|
|
1015
|
+
display: none;
|
|
1016
|
+
}
|
|
1017
|
+
#file-viewer {
|
|
1018
|
+
position: fixed;
|
|
1019
|
+
inset: 0;
|
|
1020
|
+
z-index: 100;
|
|
1021
|
+
min-width: 0;
|
|
1022
|
+
background: var(--bg);
|
|
1023
|
+
color: var(--text);
|
|
1024
|
+
display: flex;
|
|
1025
|
+
flex-direction: column;
|
|
1026
|
+
}
|
|
1027
|
+
body.file-viewer-open {
|
|
1028
|
+
overflow: hidden;
|
|
1029
|
+
}
|
|
1030
|
+
.file-viewer-header {
|
|
1031
|
+
min-height: 41px;
|
|
1032
|
+
padding: 6px 16px;
|
|
1033
|
+
background: var(--surface);
|
|
1034
|
+
border-bottom: 1px solid var(--border);
|
|
1035
|
+
display: flex;
|
|
1036
|
+
align-items: center;
|
|
1037
|
+
gap: 12px;
|
|
1038
|
+
font-size: 15px;
|
|
1039
|
+
}
|
|
1040
|
+
#file-viewer-path {
|
|
1041
|
+
min-width: 0;
|
|
1042
|
+
flex: 1;
|
|
1043
|
+
overflow: hidden;
|
|
1044
|
+
text-overflow: ellipsis;
|
|
1045
|
+
white-space: nowrap;
|
|
1046
|
+
direction: ltr;
|
|
1047
|
+
text-align: left;
|
|
1048
|
+
color: var(--text-dim);
|
|
1049
|
+
font-size: inherit;
|
|
1050
|
+
}
|
|
1051
|
+
#file-viewer-close {
|
|
1052
|
+
flex: 0 0 auto;
|
|
1053
|
+
}
|
|
1054
|
+
#file-viewer-notice {
|
|
1055
|
+
flex: 0 0 auto;
|
|
1056
|
+
padding: 6px 14px;
|
|
1057
|
+
border-bottom: 1px solid var(--border);
|
|
1058
|
+
color: var(--yellow);
|
|
1059
|
+
font-size: 13px;
|
|
1060
|
+
}
|
|
1061
|
+
#file-viewer-notice[hidden] {
|
|
1062
|
+
display: none;
|
|
1063
|
+
}
|
|
1064
|
+
#file-viewer-content {
|
|
1065
|
+
flex: 1 1 auto;
|
|
1066
|
+
min-height: 0;
|
|
1067
|
+
overflow: auto;
|
|
1068
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
1069
|
+
}
|
|
1070
|
+
.file-viewer-markdown {
|
|
1071
|
+
width: 100%;
|
|
1072
|
+
min-height: 100%;
|
|
1073
|
+
}
|
|
1074
|
+
.file-viewer-image {
|
|
1075
|
+
display: block;
|
|
1076
|
+
max-width: 100%;
|
|
1077
|
+
max-height: 100%;
|
|
1078
|
+
margin: auto;
|
|
1079
|
+
padding: 12px;
|
|
1080
|
+
object-fit: contain;
|
|
1081
|
+
}
|
|
1082
|
+
.file-viewer-code-scroll {
|
|
1083
|
+
min-height: 100%;
|
|
1084
|
+
min-width: max-content;
|
|
1085
|
+
}
|
|
1086
|
+
.file-viewer-code-grid {
|
|
1087
|
+
display: grid;
|
|
1088
|
+
grid-template-columns: max-content minmax(max-content, 1fr);
|
|
1089
|
+
width: max-content;
|
|
1090
|
+
min-width: 100%;
|
|
1091
|
+
min-height: 100%;
|
|
1092
|
+
}
|
|
1093
|
+
.file-viewer-line-numbers,
|
|
1094
|
+
.file-viewer-code {
|
|
1095
|
+
margin: 0;
|
|
1096
|
+
padding: 10px 12px;
|
|
1097
|
+
border: 0;
|
|
1098
|
+
border-radius: 0;
|
|
1099
|
+
background: var(--bg);
|
|
1100
|
+
color: var(--text);
|
|
1101
|
+
font:
|
|
1102
|
+
14px/1.55 "SF Mono",
|
|
1103
|
+
"Fira Code",
|
|
1104
|
+
monospace;
|
|
1105
|
+
white-space: pre;
|
|
1106
|
+
}
|
|
1107
|
+
.file-viewer-line-numbers {
|
|
1108
|
+
position: sticky;
|
|
1109
|
+
left: 0;
|
|
1110
|
+
z-index: 1;
|
|
1111
|
+
padding-right: 8px;
|
|
1112
|
+
background: var(--surface);
|
|
1113
|
+
border-right: 1px solid var(--border);
|
|
1114
|
+
color: var(--text-dim);
|
|
1115
|
+
text-align: right;
|
|
1116
|
+
user-select: none;
|
|
1117
|
+
}
|
|
1118
|
+
.file-viewer-code {
|
|
1119
|
+
padding-left: 12px;
|
|
1120
|
+
}
|
|
1121
|
+
.file-viewer-code code,
|
|
1122
|
+
.file-viewer-code code.hljs {
|
|
1123
|
+
padding: 0;
|
|
1124
|
+
background: transparent;
|
|
1125
|
+
font: inherit;
|
|
1126
|
+
}
|
|
1127
|
+
.file-viewer-binary-meta {
|
|
1128
|
+
padding: 18px 14px 10px;
|
|
1129
|
+
color: var(--text-dim);
|
|
1130
|
+
overflow-wrap: anywhere;
|
|
1131
|
+
}
|
|
1132
|
+
.file-viewer-download {
|
|
1133
|
+
display: inline-flex;
|
|
1134
|
+
min-height: 44px;
|
|
1135
|
+
margin: 0 14px;
|
|
1136
|
+
padding: 8px 12px;
|
|
1137
|
+
border: 1px solid var(--border);
|
|
1138
|
+
border-radius: var(--radius);
|
|
1139
|
+
align-items: center;
|
|
1140
|
+
color: var(--accent);
|
|
1141
|
+
text-decoration: none;
|
|
1142
|
+
}
|
|
1143
|
+
.file-viewer-download:hover,
|
|
1144
|
+
.file-viewer-download:focus-visible {
|
|
1145
|
+
border-color: var(--accent);
|
|
1146
|
+
}
|
|
1147
|
+
@media (min-width: 1024px) {
|
|
1148
|
+
body.file-viewer-open {
|
|
1149
|
+
padding-right: min(55vw, 800px);
|
|
1150
|
+
}
|
|
1151
|
+
#file-viewer {
|
|
1152
|
+
left: auto;
|
|
1153
|
+
width: min(55vw, 800px);
|
|
1154
|
+
border-left: 1px solid var(--border);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
993
1158
|
/* Image attach */
|
|
994
1159
|
#attach-preview {
|
|
995
1160
|
display: none;
|
package/lib/agent-key.js
ADDED
package/lib/attachments.js
CHANGED
|
@@ -70,14 +70,51 @@ function looksLikeUtf8Text(buf) {
|
|
|
70
70
|
if (byte === 0)
|
|
71
71
|
return false;
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
return decodesAsUtf8ToleratingTailTruncation(buf);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Maximum bytes one UTF-8 code point can occupy. A TRAILING code point that
|
|
77
|
+
* was cut off by the sampling window contributes at most one byte less than
|
|
78
|
+
* a full character (a complete 4-byte character would decode), so backing up
|
|
79
|
+
* `MAX_UTF8_CHAR_BYTES - 1` bytes always reaches the preceding boundary.
|
|
80
|
+
*/
|
|
81
|
+
const MAX_UTF8_CHAR_BYTES = 4;
|
|
82
|
+
/**
|
|
83
|
+
* Whether `buf` decodes cleanly as UTF-8, allowing its FINAL multi-byte
|
|
84
|
+
* character to be cut off at the buffer end.
|
|
85
|
+
*
|
|
86
|
+
* Why the tolerance: sniffMime only receives the first 4096 bytes of a
|
|
87
|
+
* larger file (the viewer's `SNIFF_BYTES` window in src/files/routes.ts and
|
|
88
|
+
* the upload-pipeline head). Content dense in multi-byte characters —
|
|
89
|
+
* Chinese, Japanese, emoji, ... — regularly leaves that window slicing
|
|
90
|
+
* clean through a character. A strict whole-buffer fatal decode then
|
|
91
|
+
* misclassifies perfectly valid UTF-8 text as binary, so the viewer
|
|
92
|
+
* force-downloads it instead of previewing it (real-world regression:
|
|
93
|
+
* ~100 of the markdown notes under ~/mine/space downloaded instead of
|
|
94
|
+
* rendering, even though every one of them was valid UTF-8).
|
|
95
|
+
*
|
|
96
|
+
* This relaxes ONLY the tail: a truncated trailing code point occupies at
|
|
97
|
+
* most 3 bytes of the buffer, so backing up at most 3 bytes reaches a
|
|
98
|
+
* character boundary — and the loop never trims the whole buffer, because an
|
|
99
|
+
* empty prefix would trivially "decode" and would turn short garbage buffers
|
|
100
|
+
* into text. NUL bytes and genuinely invalid sequences elsewhere fail every
|
|
101
|
+
* prefix attempt and still fall through to application/octet-stream — the
|
|
102
|
+
* "binary stays binary" security posture of the sniff is unchanged.
|
|
103
|
+
*/
|
|
104
|
+
function decodesAsUtf8ToleratingTailTruncation(buf) {
|
|
105
|
+
for (let end = buf.length; end >= Math.max(1, buf.length - (MAX_UTF8_CHAR_BYTES - 1)); end--) {
|
|
106
|
+
try {
|
|
107
|
+
// Fresh decoder per attempt: reuse-after-throw is not part of the
|
|
108
|
+
// TextDecoder contract across implementations.
|
|
109
|
+
new TextDecoder("utf-8", { fatal: true }).decode(buf.subarray(0, end));
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Cut deeper into the tail. Only a trailing truncated character can
|
|
114
|
+
// make a later attempt succeed — real corruption fails all of them.
|
|
115
|
+
}
|
|
80
116
|
}
|
|
117
|
+
return false;
|
|
81
118
|
}
|
|
82
119
|
/**
|
|
83
120
|
* Mimes we are willing to render inline in <img>. Everything else is forced
|
package/lib/auth-middleware.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// All whitelisted paths must be GETs that read non-sensitive data,
|
|
2
|
-
// assets needed before the user can present a token
|
|
1
|
+
// All whitelisted paths must be GETs that either read non-sensitive data,
|
|
2
|
+
// serve static assets needed before the user can present a token, or enforce
|
|
3
|
+
// independent ticket/HMAC authentication inside their route handler.
|
|
3
4
|
const WHITELIST = [
|
|
4
5
|
// Public probes
|
|
5
6
|
{ method: "GET", test: (p) => p === "/api/v1/version" },
|
|
@@ -35,6 +36,12 @@ const WHITELIST = [
|
|
|
35
36
|
method: "GET",
|
|
36
37
|
test: (p) => /^\/api\/v1\/sessions\/[A-Za-z0-9_-]+\/attachments\/[A-Za-z0-9._-]+$/.test(p),
|
|
37
38
|
},
|
|
39
|
+
// File viewer content — read-only bytes for one confirmed path,
|
|
40
|
+
// authenticated via HMAC sig+exp query string for the same reason as
|
|
41
|
+
// image GETs above (media tags / downloads cannot send Authorization
|
|
42
|
+
// headers). The content handler verifies the signature before serving:
|
|
43
|
+
// without a valid sig+exp it returns 401, never bytes.
|
|
44
|
+
{ method: "GET", test: (p) => p === "/api/v1/files/content" },
|
|
38
45
|
// --- Share viewer (public read-only snapshots) ---
|
|
39
46
|
// Viewer HTML shell + image proxy + viewer-namespaced static assets
|
|
40
47
|
// (CSS/JS) all live under /s/* — see src/share/routes.ts. The auth gate
|
package/lib/bridge.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Writable, Readable } from "node:stream";
|
|
|
3
3
|
import { EventEmitter } from "node:events";
|
|
4
4
|
import * as acp from "@agentclientprotocol/sdk";
|
|
5
5
|
import { interruptBashProc } from "./session-manager.js";
|
|
6
|
+
import { abbreviateHomePath } from "./home-path.js";
|
|
6
7
|
import { log } from "./log.js";
|
|
7
8
|
const blog = log.scope("bridge");
|
|
8
9
|
export class AgentBridge extends EventEmitter {
|
|
@@ -12,16 +13,27 @@ export class AgentBridge extends EventEmitter {
|
|
|
12
13
|
permissionRequestSessions = new Map();
|
|
13
14
|
silentSessions = new Set(); // Sessions that don't emit events
|
|
14
15
|
silentBuffers = new Map(); // Text buffers for silent sessions
|
|
16
|
+
pendingNewSessions = 0;
|
|
17
|
+
unboundNewSessionIds = new Set();
|
|
18
|
+
pendingSessionUpdates = new Map();
|
|
15
19
|
pendingAborts = new Map();
|
|
16
20
|
deadReason = null;
|
|
17
21
|
stderrTail = "";
|
|
18
22
|
closedProcesses = new WeakSet();
|
|
19
23
|
agentCmd;
|
|
24
|
+
sessionIds;
|
|
20
25
|
reloading = false;
|
|
21
26
|
attachmentDispatcher = null;
|
|
22
|
-
constructor(agentCmd) {
|
|
27
|
+
constructor(agentCmd, sessionIds) {
|
|
23
28
|
super();
|
|
24
29
|
this.agentCmd = agentCmd;
|
|
30
|
+
this.sessionIds = sessionIds;
|
|
31
|
+
}
|
|
32
|
+
agentSessionId(webSessionId) {
|
|
33
|
+
const id = this.sessionIds.getAgentSessionId(webSessionId);
|
|
34
|
+
if (!id)
|
|
35
|
+
throw new Error(`Session is not available for the current agent: ${webSessionId}`);
|
|
36
|
+
return id;
|
|
25
37
|
}
|
|
26
38
|
/**
|
|
27
39
|
* Inject the dispatcher used to translate client attachment refs into
|
|
@@ -99,29 +111,57 @@ export class AgentBridge extends EventEmitter {
|
|
|
99
111
|
async newSession(cwd, opts) {
|
|
100
112
|
if (!this.conn)
|
|
101
113
|
throw new Error("Not connected");
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
});
|
|
106
|
-
const configOptions = (session.configOptions ??
|
|
107
|
-
[]);
|
|
108
|
-
if (!opts?.silent) {
|
|
109
|
-
this.emit("event", {
|
|
110
|
-
type: "session_created",
|
|
111
|
-
sessionId: session.sessionId,
|
|
114
|
+
this.pendingNewSessions++;
|
|
115
|
+
try {
|
|
116
|
+
const session = await this.conn.newSession({
|
|
112
117
|
cwd,
|
|
113
|
-
|
|
118
|
+
mcpServers: [],
|
|
114
119
|
});
|
|
120
|
+
if (opts?.silent) {
|
|
121
|
+
this.pendingSessionUpdates.delete(session.sessionId);
|
|
122
|
+
this.silentSessions.add(session.sessionId);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
this.unboundNewSessionIds.add(session.sessionId);
|
|
126
|
+
}
|
|
127
|
+
const configOptions = (session.configOptions ??
|
|
128
|
+
[]);
|
|
129
|
+
return { sessionId: session.sessionId, configOptions };
|
|
115
130
|
}
|
|
116
|
-
|
|
131
|
+
finally {
|
|
132
|
+
this.pendingNewSessions--;
|
|
133
|
+
if (this.pendingNewSessions === 0) {
|
|
134
|
+
for (const sessionId of this.pendingSessionUpdates.keys()) {
|
|
135
|
+
if (!this.unboundNewSessionIds.has(sessionId)) {
|
|
136
|
+
this.pendingSessionUpdates.delete(sessionId);
|
|
137
|
+
blog.warn("discarded update for unrelated unmapped ACP session", {
|
|
138
|
+
sessionId,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
sessionMapped(agentSessionId) {
|
|
146
|
+
this.unboundNewSessionIds.delete(agentSessionId);
|
|
147
|
+
const updates = this.pendingSessionUpdates.get(agentSessionId) ?? [];
|
|
148
|
+
this.pendingSessionUpdates.delete(agentSessionId);
|
|
149
|
+
for (const update of updates) {
|
|
150
|
+
void this.handleSessionUpdate({ sessionId: agentSessionId, update });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
discardUnboundSession(agentSessionId) {
|
|
154
|
+
this.unboundNewSessionIds.delete(agentSessionId);
|
|
155
|
+
this.pendingSessionUpdates.delete(agentSessionId);
|
|
117
156
|
}
|
|
118
157
|
async loadSession(sessionId, cwd) {
|
|
119
158
|
if (!this.conn)
|
|
120
159
|
throw new Error("Not connected");
|
|
160
|
+
const agentSessionId = this.agentSessionId(sessionId);
|
|
121
161
|
let session;
|
|
122
162
|
try {
|
|
123
163
|
session = await this.conn.loadSession({
|
|
124
|
-
sessionId,
|
|
164
|
+
sessionId: agentSessionId,
|
|
125
165
|
cwd,
|
|
126
166
|
mcpServers: [],
|
|
127
167
|
});
|
|
@@ -144,6 +184,7 @@ export class AgentBridge extends EventEmitter {
|
|
|
144
184
|
type: "session_created",
|
|
145
185
|
sessionId,
|
|
146
186
|
cwd,
|
|
187
|
+
cwdDisplay: abbreviateHomePath(cwd),
|
|
147
188
|
configOptions,
|
|
148
189
|
});
|
|
149
190
|
return { sessionId, configOptions };
|
|
@@ -152,7 +193,19 @@ export class AgentBridge extends EventEmitter {
|
|
|
152
193
|
if (!this.conn)
|
|
153
194
|
throw new Error("Not connected");
|
|
154
195
|
const result = await this.conn.setSessionConfigOption({
|
|
155
|
-
sessionId,
|
|
196
|
+
sessionId: this.agentSessionId(sessionId),
|
|
197
|
+
configId,
|
|
198
|
+
...(typeof value === "boolean"
|
|
199
|
+
? { type: "boolean", value }
|
|
200
|
+
: { value }),
|
|
201
|
+
});
|
|
202
|
+
return result.configOptions;
|
|
203
|
+
}
|
|
204
|
+
async setAgentConfigOption(agentSessionId, configId, value) {
|
|
205
|
+
if (!this.conn)
|
|
206
|
+
throw new Error("Not connected");
|
|
207
|
+
const result = await this.conn.setSessionConfigOption({
|
|
208
|
+
sessionId: agentSessionId,
|
|
156
209
|
configId,
|
|
157
210
|
...(typeof value === "boolean"
|
|
158
211
|
? { type: "boolean", value }
|
|
@@ -196,7 +249,7 @@ export class AgentBridge extends EventEmitter {
|
|
|
196
249
|
promptParts.push({ type: "text", text });
|
|
197
250
|
const result = (await Promise.race([
|
|
198
251
|
this.conn.prompt({
|
|
199
|
-
sessionId,
|
|
252
|
+
sessionId: this.agentSessionId(sessionId),
|
|
200
253
|
prompt: promptParts,
|
|
201
254
|
}),
|
|
202
255
|
abortPromise,
|
|
@@ -241,7 +294,10 @@ export class AgentBridge extends EventEmitter {
|
|
|
241
294
|
this.denyPermission(requestId);
|
|
242
295
|
}
|
|
243
296
|
}
|
|
244
|
-
await this.conn?.cancel({ sessionId });
|
|
297
|
+
await this.conn?.cancel({ sessionId: this.agentSessionId(sessionId) });
|
|
298
|
+
}
|
|
299
|
+
async cancelAgentSession(agentSessionId) {
|
|
300
|
+
await this.conn?.cancel({ sessionId: agentSessionId });
|
|
245
301
|
}
|
|
246
302
|
/**
|
|
247
303
|
* Mark the agent subprocess as dead. Rejects in-flight prompts and emits
|
|
@@ -374,6 +430,8 @@ export class AgentBridge extends EventEmitter {
|
|
|
374
430
|
// 4. Clean up bridge-side silent session state
|
|
375
431
|
this.silentSessions.clear();
|
|
376
432
|
this.silentBuffers.clear();
|
|
433
|
+
this.unboundNewSessionIds.clear();
|
|
434
|
+
this.pendingSessionUpdates.clear();
|
|
377
435
|
// 5. Invalidate title service session
|
|
378
436
|
titleService.invalidate();
|
|
379
437
|
// 5. Clear liveSessions so ensureResumed() will re-register on next access
|
|
@@ -457,6 +515,10 @@ export class AgentBridge extends EventEmitter {
|
|
|
457
515
|
}
|
|
458
516
|
// --- ACP Client callbacks ---
|
|
459
517
|
handlePermission(params) {
|
|
518
|
+
const webSessionId = this.sessionIds.getWebSessionId(params.sessionId);
|
|
519
|
+
if (!webSessionId) {
|
|
520
|
+
return Promise.resolve({ outcome: { outcome: "cancelled" } });
|
|
521
|
+
}
|
|
460
522
|
const requestId = crypto.randomUUID();
|
|
461
523
|
const toolCall = params.toolCall;
|
|
462
524
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- toolCall may be undefined in practice
|
|
@@ -467,11 +529,11 @@ export class AgentBridge extends EventEmitter {
|
|
|
467
529
|
return new Promise((resolve) => {
|
|
468
530
|
// Register resolver BEFORE emitting, so synchronous auto-approve can find it
|
|
469
531
|
this.permissionResolvers.set(requestId, resolve);
|
|
470
|
-
this.permissionRequestSessions.set(requestId,
|
|
532
|
+
this.permissionRequestSessions.set(requestId, webSessionId);
|
|
471
533
|
this.emit("event", {
|
|
472
534
|
type: "permission_request",
|
|
473
535
|
requestId,
|
|
474
|
-
sessionId:
|
|
536
|
+
sessionId: webSessionId,
|
|
475
537
|
title,
|
|
476
538
|
toolCallId,
|
|
477
539
|
options: params.options,
|
|
@@ -484,9 +546,23 @@ export class AgentBridge extends EventEmitter {
|
|
|
484
546
|
}
|
|
485
547
|
handleSessionUpdate(params) {
|
|
486
548
|
const update = params.update;
|
|
487
|
-
const
|
|
488
|
-
if (this.silentSessions.has(
|
|
489
|
-
this.captureSilentText(
|
|
549
|
+
const agentSessionId = params.sessionId;
|
|
550
|
+
if (this.silentSessions.has(agentSessionId)) {
|
|
551
|
+
this.captureSilentText(agentSessionId, update);
|
|
552
|
+
return Promise.resolve();
|
|
553
|
+
}
|
|
554
|
+
const sessionId = this.sessionIds.getWebSessionId(agentSessionId);
|
|
555
|
+
if (!sessionId) {
|
|
556
|
+
if (this.pendingNewSessions > 0 ||
|
|
557
|
+
this.unboundNewSessionIds.has(agentSessionId)) {
|
|
558
|
+
const updates = this.pendingSessionUpdates.get(agentSessionId) ?? [];
|
|
559
|
+
updates.push(update);
|
|
560
|
+
this.pendingSessionUpdates.set(agentSessionId, updates);
|
|
561
|
+
return Promise.resolve();
|
|
562
|
+
}
|
|
563
|
+
blog.warn("ignored event for unmapped ACP session", {
|
|
564
|
+
sessionId: agentSessionId,
|
|
565
|
+
});
|
|
490
566
|
return Promise.resolve();
|
|
491
567
|
}
|
|
492
568
|
const event = this.sessionUpdateToEvent(sessionId, update);
|
|
@@ -528,9 +604,26 @@ export class AgentBridge extends EventEmitter {
|
|
|
528
604
|
id: update.toolCallId,
|
|
529
605
|
status: update.status ?? "",
|
|
530
606
|
content: (update.content ?? undefined),
|
|
607
|
+
...(typeof update.title === "string" ? { title: update.title } : {}),
|
|
608
|
+
...(typeof update.kind === "string" ? { kind: update.kind } : {}),
|
|
609
|
+
...(update.rawInput ? { rawInput: update.rawInput } : {}),
|
|
610
|
+
...(Object.hasOwn(update, "rawOutput")
|
|
611
|
+
? { rawOutput: update.rawOutput }
|
|
612
|
+
: {}),
|
|
613
|
+
...(Array.isArray(update.locations)
|
|
614
|
+
? { locations: update.locations }
|
|
615
|
+
: {}),
|
|
531
616
|
};
|
|
532
617
|
case "plan":
|
|
533
618
|
return { type: "plan", sessionId, entries: update.entries };
|
|
619
|
+
case "usage_update":
|
|
620
|
+
return {
|
|
621
|
+
type: "usage_update",
|
|
622
|
+
sessionId,
|
|
623
|
+
used: update.used,
|
|
624
|
+
size: update.size,
|
|
625
|
+
cost: update.cost,
|
|
626
|
+
};
|
|
534
627
|
case "config_option_update":
|
|
535
628
|
return {
|
|
536
629
|
type: "config_option_update",
|
package/lib/event-handler.js
CHANGED
|
@@ -40,6 +40,17 @@ function handleToolCall(event, sessions, store) {
|
|
|
40
40
|
rawInput: event.rawInput,
|
|
41
41
|
}, { from_ref: "agent" });
|
|
42
42
|
}
|
|
43
|
+
function handleUsageUpdate(event, sessions) {
|
|
44
|
+
sessions.state.patch(event.sessionId, {
|
|
45
|
+
runtime: {
|
|
46
|
+
contextUsage: {
|
|
47
|
+
used: event.used,
|
|
48
|
+
size: event.size,
|
|
49
|
+
...(event.cost !== undefined ? { cost: event.cost } : {}),
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
43
54
|
function handlePlan(event, sessions, store) {
|
|
44
55
|
sessions.flushBuffers(event.sessionId);
|
|
45
56
|
sessions.state.patch(event.sessionId, {
|
|
@@ -177,7 +188,7 @@ function handlePromptDone(event, sessions, store) {
|
|
|
177
188
|
...(event.promptId ? { promptId: event.promptId } : {}),
|
|
178
189
|
}, { from_ref: "agent" });
|
|
179
190
|
}
|
|
180
|
-
function handleError(event, sessions) {
|
|
191
|
+
function handleError(event, sessions, store) {
|
|
181
192
|
if (event.sessionId) {
|
|
182
193
|
// Same attribution as a completion: a superseded turn failing late must
|
|
183
194
|
// not end the turn that replaced it. The buffered tail still flushes.
|
|
@@ -196,6 +207,10 @@ function handleError(event, sessions) {
|
|
|
196
207
|
sessions.state.patch(event.sessionId, {
|
|
197
208
|
runtime: { streaming: { assistant: false, thinking: false } },
|
|
198
209
|
});
|
|
210
|
+
store.saveEvent(event.sessionId, event.type, {
|
|
211
|
+
message: event.message,
|
|
212
|
+
...(event.promptId ? { promptId: event.promptId } : {}),
|
|
213
|
+
}, { from_ref: "agent" });
|
|
199
214
|
}
|
|
200
215
|
}
|
|
201
216
|
function dispatchAgentEvent(event, sessions, store, bridge, config, sseManager) {
|
|
@@ -218,7 +233,15 @@ function dispatchAgentEvent(event, sessions, store, bridge, config, sseManager)
|
|
|
218
233
|
handleToolCall(event, sessions, store);
|
|
219
234
|
return false;
|
|
220
235
|
case "tool_call_update":
|
|
221
|
-
store.saveEvent(event.sessionId, event.type, {
|
|
236
|
+
store.saveEvent(event.sessionId, event.type, {
|
|
237
|
+
id: event.id,
|
|
238
|
+
status: event.status,
|
|
239
|
+
content: event.content,
|
|
240
|
+
title: event.title,
|
|
241
|
+
kind: event.kind,
|
|
242
|
+
rawInput: event.rawInput,
|
|
243
|
+
locations: event.locations,
|
|
244
|
+
}, { from_ref: "agent" });
|
|
222
245
|
return false;
|
|
223
246
|
case "plan":
|
|
224
247
|
handlePlan(event, sessions, store);
|
|
@@ -229,7 +252,7 @@ function dispatchAgentEvent(event, sessions, store, bridge, config, sseManager)
|
|
|
229
252
|
handlePromptDone(event, sessions, store);
|
|
230
253
|
return false;
|
|
231
254
|
case "error":
|
|
232
|
-
handleError(event, sessions);
|
|
255
|
+
handleError(event, sessions, store);
|
|
233
256
|
return false;
|
|
234
257
|
}
|
|
235
258
|
return false;
|
|
@@ -253,6 +276,10 @@ function maybePushNotify(event, pushService) {
|
|
|
253
276
|
});
|
|
254
277
|
}
|
|
255
278
|
export function handleAgentEvent(event, sessions, store, bridge, config, sseManager, pushService, _clientRegistry) {
|
|
279
|
+
if (event.type === "usage_update") {
|
|
280
|
+
handleUsageUpdate(event, sessions);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
256
283
|
if (event.type === "available_commands_update") {
|
|
257
284
|
const snapshot = sessions.updateAgentCommands(event.sessionId, event.commands);
|
|
258
285
|
if (sessions.restoringSessions.has(event.sessionId))
|
|
@@ -266,6 +293,7 @@ export function handleAgentEvent(event, sessions, store, bridge, config, sseMana
|
|
|
266
293
|
}
|
|
267
294
|
sessions.state.clearStreaming();
|
|
268
295
|
sessions.state.clearPlans();
|
|
296
|
+
sessions.state.clearContextUsage();
|
|
269
297
|
for (const snapshot of sessions.clearAgentCommands()) {
|
|
270
298
|
sseManager.broadcast({
|
|
271
299
|
type: "available_commands_update",
|