@lelouchhe/webagent 0.5.1 → 0.7.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.
@@ -0,0 +1 @@
1
+ var s=Object.freeze({OK:200,CREATED:201,ACCEPTED:202,NO_CONTENT:204,BAD_REQUEST:400,UNAUTHORIZED:401,FORBIDDEN:403,NOT_FOUND:404,CONFLICT:409,GONE:410,PAYLOAD_TOO_LARGE:413,UNPROCESSABLE_CONTENT:422,INTERNAL_SERVER_ERROR:500,NOT_IMPLEMENTED:501,SERVICE_UNAVAILABLE:503});var c="wa_token";async function p(n,a={}){let i=a.fetch??fetch,o=n.trim();if(!o)return{ok:!1,error:"Token is required"};let r;try{r=await i("/api/v1/auth/verify",{method:"GET",headers:{Authorization:`Bearer ${o}`}})}catch(e){return{ok:!1,error:`Network error: ${e instanceof Error?e.message:String(e)}`}}if(r.status===s.OK){let e;try{e=await r.json()}catch{return{ok:!1,error:"Server returned invalid response"}}if(!e.ok||typeof e.name!="string"||typeof e.scope!="string")return{ok:!1,error:"Server returned malformed response"};try{localStorage.setItem(c,o)}catch(t){return{ok:!1,error:`Could not store token: ${t instanceof Error?t.message:String(t)}`}}return{ok:!0,name:e.name,scope:e.scope}}return r.status===s.UNAUTHORIZED?{ok:!1,error:"Invalid or rejected token"}:{ok:!1,error:`Unexpected response (status ${r.status})`}}export{s as a,c as b,p as c};
@@ -1 +1 @@
1
- import{a as r,b as i}from"./chunk.D4ZYHJAM.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
+ import{b as r,c as i}from"./chunk.UMQMOGWO.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());
@@ -0,0 +1 @@
1
+ import{b as f,k as g,l as h}from"./chunk.3CCHZCG7.js";import{a as u}from"./chunk.UMQMOGWO.js";import"./chunk.AJZBJBMO.js";var E=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function m(t){return String(t).padStart(2,"0")}function p(t,r){if(!t)return"";let s=new Date(t),a=s.getTime();if(isNaN(a))return"";let l=Math.floor((r.getTime()-a)/1e3);if(l<60)return"just now";let i=Math.floor(l/60);if(i<60)return`${i}m ago`;let o=Math.floor(i/60);if(o<24)return`${o}h ago`;let e=Math.floor(o/24);if(e<7)return`${e}d ago`;let n=s.getUTCFullYear()===r.getUTCFullYear(),c=E[s.getUTCMonth()],d=s.getUTCDate();return n?`${c} ${d}`:`${c} ${d}, ${s.getUTCFullYear()}`}function y(t){if(!t)return"";let r=new Date(t);return isNaN(r.getTime())?"":`${r.getUTCFullYear()}-${m(r.getUTCMonth()+1)}-${m(r.getUTCDate())} ${m(r.getUTCHours())}:${m(r.getUTCMinutes())} UTC`}function T(t){let r=/^\/api\/v1\/sessions\/[^/]+\/attachments\/([A-Za-z0-9._-]+)(?:\?.*)?$/;return s=>{let a=r.exec(s);return a?`/s/${encodeURIComponent(t)}/attachments/${a[1]}`:s}}function C(t){if(typeof t.data=="string")try{return JSON.parse(t.data)}catch{return{}}return t.data}function v(t,r,s){let a=new Set;for(let n of t)if(n.type==="permission_response")try{let c=JSON.parse(n.data);typeof c.requestId=="string"&&a.add(c.requestId)}catch{}let l=new Map,i=new Map,o=null,e={rewriteAttachmentSrc:T(s),enhanceMarkdown:f,findToolCallEl:n=>l.get(n)??null,findPermissionEl:n=>i.get(n)??null,findBashEl:()=>o,isPermissionResolved:n=>a.has(n)};for(let n of t){if(!g(n.type))continue;let c=C(n),d=h(n.type,c,e);d?(r.appendChild(d),n.type==="tool_call"&&typeof c.id=="string"?l.set(c.id,d):n.type==="permission_request"&&typeof c.requestId=="string"?i.set(c.requestId,d):n.type==="bash_command"&&(o=d)):n.type==="bash_result"&&(o=null)}}async function S(){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 r=t[1],s=document.getElementById("session-info"),a=document.getElementById("messages"),l=document.querySelector(".share-footer-author"),i=document.querySelector(".share-footer-meta");if(!a)return;let o;try{let e=await fetch(`/api/v1/shared/${encodeURIComponent(r)}/events`,{headers:{Accept:"application/json"},credentials:"omit"});if(e.status===u.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,a,r),i){let e=o.share.shared_at??o.share.created_at;i.textContent=p(e,new Date),i instanceof HTMLElement&&(i.title=y(e))}}S();
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.00xfh3e6.css" />
20
+ <link rel="stylesheet" href="/styles.01ky3cex.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 &lt;name&gt;</code> on the server.
45
45
  </p>
46
46
  </div>
47
- <script type="module" src="/js/login.PYIK52HN.js"></script>
47
+ <script type="module" src="/js/login.2WA6DTGM.js"></script>
48
48
  </body>
49
49
  </html>
@@ -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.00xfh3e6.css" />
19
+ <link rel="stylesheet" href="/s/_/styles.01ky3cex.css" />
20
20
  <link rel="stylesheet" href="/s/_/share-viewer.00gubshk.css" />
21
- <link rel="modulepreload" href="/s/_/chunk.IJM5DBCO.js">
22
- <link rel="modulepreload" href="/s/_/chunk.D4ZYHJAM.js">
21
+ <link rel="modulepreload" href="/s/_/chunk.3CCHZCG7.js">
22
+ <link rel="modulepreload" href="/s/_/chunk.UMQMOGWO.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.FCSSTUVY.js"></script>
52
+ <script type="module" src="/s/_/viewer.T32M5AZZ.js"></script>
53
53
  </body>
54
54
  </html>
@@ -177,6 +177,7 @@ body {
177
177
  display: flex;
178
178
  flex-direction: column;
179
179
  gap: 4px;
180
+ overflow-anchor: none;
180
181
  }
181
182
  #messages > * {
182
183
  min-width: 0;
@@ -1005,10 +1006,17 @@ body {
1005
1006
 
1006
1007
  /* History pagination sentinel */
1007
1008
  .history-sentinel {
1009
+ height: 1px;
1010
+ padding: 0;
1011
+ opacity: 0;
1012
+ pointer-events: none;
1013
+ }
1014
+ .history-loading {
1008
1015
  text-align: center;
1009
1016
  padding: 8px;
1010
1017
  opacity: 0.5;
1011
1018
  font-size: 0.85em;
1019
+ pointer-events: none;
1012
1020
  }
1013
1021
 
1014
1022
  /* Utility: dim out cancelled / settled / past states. Used in dynamic innerHTML
@@ -0,0 +1,27 @@
1
+ function splitFirstToken(text) {
2
+ const boundary = text.search(/\s/);
3
+ if (boundary < 0)
4
+ return { token: text, remainder: "" };
5
+ return {
6
+ token: text.slice(0, boundary),
7
+ remainder: text.slice(boundary),
8
+ };
9
+ }
10
+ export function resolveAgentCommand(text, commands) {
11
+ if (!text.startsWith("//"))
12
+ return null;
13
+ const { token, remainder } = splitFirstToken(text);
14
+ const inputName = token.slice(2);
15
+ if (!inputName)
16
+ return null;
17
+ const command = commands.find((candidate) => candidate.name.toLowerCase() === inputName.toLowerCase());
18
+ if (!command)
19
+ return null;
20
+ return {
21
+ command: token,
22
+ agentText: `/${command.name}${remainder}`,
23
+ };
24
+ }
25
+ export function agentCommandToken(text) {
26
+ return splitFirstToken(text).token || text;
27
+ }
package/lib/bridge.js CHANGED
@@ -95,10 +95,10 @@ export class AgentBridge extends EventEmitter {
95
95
  async newSession(cwd, opts) {
96
96
  if (!this.conn)
97
97
  throw new Error("Not connected");
98
- const session = (await this.conn.newSession({
98
+ const session = await this.conn.newSession({
99
99
  cwd,
100
100
  mcpServers: [],
101
- }));
101
+ });
102
102
  const configOptions = (session.configOptions ??
103
103
  []);
104
104
  if (!opts?.silent) {
@@ -116,11 +116,11 @@ export class AgentBridge extends EventEmitter {
116
116
  throw new Error("Not connected");
117
117
  let session;
118
118
  try {
119
- session = (await this.conn.loadSession({
119
+ session = await this.conn.loadSession({
120
120
  sessionId,
121
121
  cwd,
122
122
  mcpServers: [],
123
- }));
123
+ });
124
124
  }
125
125
  catch (err) {
126
126
  // -32002 = Resource not found. Some agents (e.g. claude-agent-acp) don't
@@ -147,11 +147,13 @@ export class AgentBridge extends EventEmitter {
147
147
  async setConfigOption(sessionId, configId, value) {
148
148
  if (!this.conn)
149
149
  throw new Error("Not connected");
150
- const result = (await this.conn.setSessionConfigOption({
150
+ const result = await this.conn.setSessionConfigOption({
151
151
  sessionId,
152
152
  configId,
153
- value,
154
- }));
153
+ ...(typeof value === "boolean"
154
+ ? { type: "boolean", value }
155
+ : { value }),
156
+ });
155
157
  return result.configOptions;
156
158
  }
157
159
  async prompt(sessionId, text, attachments) {
@@ -257,6 +259,7 @@ export class AgentBridge extends EventEmitter {
257
259
  });
258
260
  abort(new Error(reason));
259
261
  }
262
+ this.emit("event", { type: "agent_disconnected" });
260
263
  this.conn = null;
261
264
  }
262
265
  /** Send a prompt and collect the full text response without emitting events. */
@@ -497,6 +500,16 @@ export class AgentBridge extends EventEmitter {
497
500
  configOptions: update
498
501
  .configOptions ?? [],
499
502
  };
503
+ case "available_commands_update":
504
+ return {
505
+ type: "available_commands_update",
506
+ sessionId,
507
+ commands: update.availableCommands.map((command) => ({
508
+ name: command.name,
509
+ description: command.description,
510
+ ...(command.input ? { input: { hint: command.input.hint } } : {}),
511
+ })),
512
+ };
500
513
  default:
501
514
  return null;
502
515
  }
@@ -14,7 +14,9 @@ function handleConfigLikeEvent(event, sessions, store) {
14
14
  if (event.configOptions.length)
15
15
  sessions.cachedConfigOptions = event.configOptions;
16
16
  for (const opt of event.configOptions) {
17
- store.updateSessionConfig(event.sessionId, opt.id, opt.currentValue);
17
+ if (typeof opt.currentValue === "string") {
18
+ store.updateSessionConfig(event.sessionId, opt.id, opt.currentValue);
19
+ }
18
20
  }
19
21
  }
20
22
  function handleMessageChunk(event, sessions) {
@@ -211,6 +213,21 @@ function maybePushNotify(event, pushService) {
211
213
  });
212
214
  }
213
215
  export function handleAgentEvent(event, sessions, store, bridge, config, sseManager, pushService, _clientRegistry) {
216
+ if (event.type === "available_commands_update") {
217
+ const snapshot = sessions.updateAgentCommands(event.sessionId, event.commands);
218
+ if (sessions.restoringSessions.has(event.sessionId))
219
+ return;
220
+ sseManager.broadcast({ ...event, ...snapshot });
221
+ return;
222
+ }
223
+ if (event.type === "agent_reloading" || event.type === "agent_disconnected") {
224
+ for (const snapshot of sessions.clearAgentCommands()) {
225
+ sseManager.broadcast({
226
+ type: "available_commands_update",
227
+ ...snapshot,
228
+ });
229
+ }
230
+ }
214
231
  if ("sessionId" in event &&
215
232
  event.sessionId &&
216
233
  sessions.restoringSessions.has(event.sessionId))
@@ -0,0 +1,17 @@
1
+ export const HTTP_STATUS = Object.freeze({
2
+ OK: 200,
3
+ CREATED: 201,
4
+ ACCEPTED: 202,
5
+ NO_CONTENT: 204,
6
+ BAD_REQUEST: 400,
7
+ UNAUTHORIZED: 401,
8
+ FORBIDDEN: 403,
9
+ NOT_FOUND: 404,
10
+ CONFLICT: 409,
11
+ GONE: 410,
12
+ PAYLOAD_TOO_LARGE: 413,
13
+ UNPROCESSABLE_CONTENT: 422,
14
+ INTERNAL_SERVER_ERROR: 500,
15
+ NOT_IMPLEMENTED: 501,
16
+ SERVICE_UNAVAILABLE: 503,
17
+ });
@@ -5,7 +5,7 @@ export function pickModelByPatterns(configOptions, patterns) {
5
5
  .filter((p) => p.length > 0);
6
6
  if (normalized.length === 0)
7
7
  return null;
8
- const modelOpt = configOptions.find((c) => c.id === "model");
8
+ const modelOpt = configOptions.find((c) => c.id === "model" && "options" in c);
9
9
  if (!modelOpt || modelOpt.options.length === 0)
10
10
  return null;
11
11
  for (const pattern of normalized) {
@@ -3,6 +3,7 @@ import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { createHash } from "node:crypto";
4
4
  import { join } from "node:path";
5
5
  import { log } from "./log.js";
6
+ import { HTTP_STATUS } from "./http-status.js";
6
7
  const slog = log.scope("push");
7
8
  const elog = log.scope("egress");
8
9
  const VAPID_FILE = "vapid.json";
@@ -267,7 +268,7 @@ export class PushService {
267
268
  else {
268
269
  fail++;
269
270
  const err = result.reason;
270
- if (err.statusCode === 410) {
271
+ if (err.statusCode === HTTP_STATUS.GONE) {
271
272
  fail410++;
272
273
  this.store.removeSubscription(endpoint);
273
274
  this.failureCounts.delete(endpoint);