@lelouchhe/webagent 0.4.0 → 0.6.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/lib/routes.js CHANGED
@@ -17,6 +17,7 @@ const slog = rlog.scope("session");
17
17
  const mlog = rlog.scope("msg");
18
18
  import { signAttachmentUrl, verifyAttachmentSig, reSignAttachmentUrlsInJson, } from "./auth.js";
19
19
  import { buildContentDisposition, classifyKind, isInlineMime, mimeToExt, normalizeDisplayName, sniffMime, } from "./attachments.js";
20
+ import { readImageDimensions } from "./image-dimensions.js";
20
21
  const IS_WIN = process.platform === "win32";
21
22
  const SAFE_ID = /^[a-zA-Z0-9_-]+$/;
22
23
  const MIME = {
@@ -30,6 +31,8 @@ const MIME = {
30
31
  ".jpeg": "image/jpeg",
31
32
  ".gif": "image/gif",
32
33
  ".webp": "image/webp",
34
+ ".woff2": "font/woff2",
35
+ ".wasm": "application/wasm",
33
36
  };
34
37
  /**
35
38
  * HTML entrypoints served by this app. Any new HTML page MUST be registered
@@ -51,7 +54,10 @@ export const HTML_ENTRYPOINTS = [
51
54
  *
52
55
  * - default-src 'self': everything same-origin only
53
56
  * - img-src adds data: + blob: for image-upload preview
54
- * - script-src 'self' (no inline; theme bootstrap is /theme-init.js)
57
+ * - script-src 'self' 'wasm-unsafe-eval' (no inline; theme bootstrap is
58
+ * /theme-init.js. 'wasm-unsafe-eval' is the narrow CSP3 token that
59
+ * permits WebAssembly.instantiate() WITHOUT re-enabling eval()/Function;
60
+ * needed by any wasm consumer.)
55
61
  * - style-src 'self' (no inline; login styles live in /styles.css)
56
62
  * - object-src 'none', frame-ancestors 'none', base-uri 'self', form-action 'self'
57
63
  * - connect-src 'self' for fetch + EventSource
@@ -59,7 +65,7 @@ export const HTML_ENTRYPOINTS = [
59
65
  export const CSP_POLICY = [
60
66
  "default-src 'self'",
61
67
  "img-src 'self' data: blob:",
62
- "script-src 'self'",
68
+ "script-src 'self' 'wasm-unsafe-eval'",
63
69
  "style-src 'self'",
64
70
  "connect-src 'self'",
65
71
  "object-src 'none'",
@@ -91,6 +97,20 @@ function getClientOpId(req) {
91
97
  return v;
92
98
  return null;
93
99
  }
100
+ function logPromptRejectBeforeSave(fields) {
101
+ plog.warn("rejected before save", {
102
+ sessionId: fields.sessionId.slice(0, 8),
103
+ status: fields.status,
104
+ reason: fields.reason,
105
+ ...(fields.opId ? { opId: fields.opId } : {}),
106
+ ...(fields.textLength != null ? { textLength: fields.textLength } : {}),
107
+ ...(fields.attachmentCount != null
108
+ ? { attachmentCount: fields.attachmentCount }
109
+ : {}),
110
+ ...(fields.busyKind ? { busyKind: fields.busyKind } : {}),
111
+ ...(fields.error ? { error: fields.error } : {}),
112
+ });
113
+ }
94
114
  function tryReplayClientOp(req, res, store, sessionId) {
95
115
  const opId = getClientOpId(req);
96
116
  if (!opId)
@@ -317,6 +337,7 @@ async function handleAttachmentUpload(req, res, sessionId, deps) {
317
337
  finalPath = join(dir, `${uploadId}.${fileExt}`);
318
338
  }
319
339
  }
340
+ const imageDimensions = kind === "image" ? readImageDimensions(head) : null;
320
341
  await rename(tmpPath, finalPath);
321
342
  const rp = await realpath(finalPath);
322
343
  const row = store.insertAttachment({
@@ -327,6 +348,8 @@ async function handleAttachmentUpload(req, res, sessionId, deps) {
327
348
  mime: fileMime,
328
349
  size: bytesWritten,
329
350
  realpath: rp,
351
+ width: imageDimensions?.width ?? null,
352
+ height: imageDimensions?.height ?? null,
330
353
  });
331
354
  // Invalidate the per-session attachment label cache so the
332
355
  // next egress (SSE broadcast or replay) sees this new row.
@@ -344,6 +367,8 @@ async function handleAttachmentUpload(req, res, sessionId, deps) {
344
367
  displayName: row.name,
345
368
  mimeType: row.mime,
346
369
  size: row.size,
370
+ width: row.width,
371
+ height: row.height,
347
372
  kind: row.kind,
348
373
  path: `sessions/${sessionId}/attachments/${fileName}`,
349
374
  url: fileUrl,
@@ -779,17 +804,36 @@ export function createRequestHandler(deps) {
779
804
  const promptMatch = url.match(/^\/api\/v1\/sessions\/([^/]+)\/prompt\/?(\?.*)?$/);
780
805
  if (promptMatch && req.method === "POST") {
781
806
  const sessionId = decodeURIComponent(promptMatch[1]);
807
+ const requestOpId = getClientOpId(req);
782
808
  const session = store.getSession(sessionId);
783
809
  if (!session) {
810
+ logPromptRejectBeforeSave({
811
+ sessionId,
812
+ status: 404,
813
+ reason: "session_not_found",
814
+ opId: requestOpId,
815
+ });
784
816
  json(res, 404, { error: "Session not found" });
785
817
  return;
786
818
  }
787
819
  const bridge = getBridge?.();
788
820
  if (!bridge) {
821
+ logPromptRejectBeforeSave({
822
+ sessionId,
823
+ status: 503,
824
+ reason: "agent_not_ready",
825
+ opId: requestOpId,
826
+ });
789
827
  json(res, 503, { error: "Agent not ready yet" });
790
828
  return;
791
829
  }
792
830
  if (!sessions) {
831
+ logPromptRejectBeforeSave({
832
+ sessionId,
833
+ status: 503,
834
+ reason: "session_manager_unavailable",
835
+ opId: requestOpId,
836
+ });
793
837
  json(res, 503, { error: "Session manager not available" });
794
838
  return;
795
839
  }
@@ -801,6 +845,13 @@ export function createRequestHandler(deps) {
801
845
  await sessions.ensureResumed(bridge, sessionId);
802
846
  }
803
847
  catch (err) {
848
+ logPromptRejectBeforeSave({
849
+ sessionId,
850
+ status: 500,
851
+ reason: "resume_failed",
852
+ opId,
853
+ error: errorMessage(err),
854
+ });
804
855
  json(res, 500, {
805
856
  error: `Failed to resume session: ${err instanceof Error ? err.message : String(err)}`,
806
857
  });
@@ -809,6 +860,13 @@ export function createRequestHandler(deps) {
809
860
  // Check if session is busy
810
861
  const busyKind = sessions.getBusyKind(sessionId);
811
862
  if (busyKind) {
863
+ logPromptRejectBeforeSave({
864
+ sessionId,
865
+ status: 409,
866
+ reason: "session_busy",
867
+ opId,
868
+ busyKind,
869
+ });
812
870
  json(res, 409, { error: "Session is busy", busyKind });
813
871
  return;
814
872
  }
@@ -817,10 +875,26 @@ export function createRequestHandler(deps) {
817
875
  body = JSON.parse(await readBody(req));
818
876
  }
819
877
  catch {
878
+ logPromptRejectBeforeSave({
879
+ sessionId,
880
+ status: 400,
881
+ reason: "invalid_json",
882
+ opId,
883
+ });
820
884
  json(res, 400, { error: "Invalid JSON" });
821
885
  return;
822
886
  }
823
887
  if (!body.text) {
888
+ logPromptRejectBeforeSave({
889
+ sessionId,
890
+ status: 400,
891
+ reason: "missing_text",
892
+ opId,
893
+ textLength: 0,
894
+ attachmentCount: Array.isArray(body.attachments)
895
+ ? body.attachments.length
896
+ : undefined,
897
+ });
824
898
  json(res, 400, { error: "Missing required field: text" });
825
899
  return;
826
900
  }
@@ -830,6 +904,13 @@ export function createRequestHandler(deps) {
830
904
  const attachments = body.attachments;
831
905
  if (attachments) {
832
906
  if (!Array.isArray(attachments)) {
907
+ logPromptRejectBeforeSave({
908
+ sessionId,
909
+ status: 400,
910
+ reason: "attachments_not_array",
911
+ opId,
912
+ textLength: body.text.length,
913
+ });
833
914
  json(res, 400, { error: "attachments must be an array" });
834
915
  return;
835
916
  }
@@ -841,14 +922,32 @@ export function createRequestHandler(deps) {
841
922
  typeof att.attachmentId !== "string" ||
842
923
  typeof att.displayName !== "string" ||
843
924
  typeof att.mimeType !== "string") {
925
+ logPromptRejectBeforeSave({
926
+ sessionId,
927
+ status: 400,
928
+ reason: "invalid_attachment_entry",
929
+ opId,
930
+ textLength: body.text.length,
931
+ attachmentCount: attachments.length,
932
+ });
844
933
  json(res, 400, { error: "Invalid attachment entry" });
845
934
  return;
846
935
  }
847
936
  if (typeof att.uri === "string" ||
848
937
  typeof att.data === "string" ||
849
- typeof att.path === "string") {
938
+ typeof att.path === "string" ||
939
+ typeof att.width === "number" ||
940
+ typeof att.height === "number") {
941
+ logPromptRejectBeforeSave({
942
+ sessionId,
943
+ status: 400,
944
+ reason: "client_supplied_attachment_data",
945
+ opId,
946
+ textLength: body.text.length,
947
+ attachmentCount: attachments.length,
948
+ });
850
949
  json(res, 400, {
851
- error: "Client must not supply uri/data/path",
950
+ error: "Client must not supply uri/data/path/width/height",
852
951
  });
853
952
  return;
854
953
  }
@@ -873,6 +972,9 @@ export function createRequestHandler(deps) {
873
972
  displayName: a.displayName,
874
973
  mimeType: a.mimeType,
875
974
  path: `/api/v1/sessions/${sessionId}/attachments/${fileName}`,
975
+ ...(row.width != null && row.height != null
976
+ ? { width: row.width, height: row.height }
977
+ : {}),
876
978
  },
877
979
  ];
878
980
  });
@@ -1043,11 +1145,14 @@ export function createRequestHandler(deps) {
1043
1145
  return;
1044
1146
  }
1045
1147
  // --- PUT /api/v1/sessions/:id/{model,mode,reasoning-effort} ---
1046
- const configPutMatch = url.match(/^\/api\/v1\/sessions\/([^/]+)\/(model|mode|reasoning-effort)\/?$/);
1148
+ // --- PUT /api/v1/sessions/:id/config/:configId ---
1149
+ const legacyConfigPutMatch = url.match(/^\/api\/v1\/sessions\/([^/]+)\/(model|mode|reasoning-effort)\/?$/);
1150
+ const genericConfigPutMatch = url.match(/^\/api\/v1\/sessions\/([^/]+)\/config\/([^/]+)\/?$/);
1151
+ const configPutMatch = legacyConfigPutMatch ?? genericConfigPutMatch;
1047
1152
  if (configPutMatch && req.method === "PUT") {
1048
1153
  const sessionId = decodeURIComponent(configPutMatch[1]);
1049
- const configPath = configPutMatch[2];
1050
- const configId = configPath === "reasoning-effort" ? "reasoning_effort" : configPath;
1154
+ const configPath = decodeURIComponent(configPutMatch[2]);
1155
+ const configId = configPath.replace(/-/g, "_");
1051
1156
  const session = store.getSession(sessionId);
1052
1157
  if (!session) {
1053
1158
  json(res, 404, { error: "Session not found" });
@@ -1066,14 +1171,16 @@ export function createRequestHandler(deps) {
1066
1171
  json(res, 400, { error: "Invalid JSON" });
1067
1172
  return;
1068
1173
  }
1069
- if (!body.value) {
1174
+ if (body.value === undefined) {
1070
1175
  json(res, 400, { error: "Missing required field: value" });
1071
1176
  return;
1072
1177
  }
1073
1178
  try {
1074
1179
  const configOptions = await bridge.setConfigOption(sessionId, configId, body.value);
1075
1180
  for (const opt of configOptions) {
1076
- store.updateSessionConfig(sessionId, opt.id, opt.currentValue);
1181
+ if (typeof opt.currentValue === "string") {
1182
+ store.updateSessionConfig(sessionId, opt.id, opt.currentValue);
1183
+ }
1077
1184
  }
1078
1185
  sseManager.broadcast({
1079
1186
  type: "config_option_update",
@@ -1170,7 +1277,9 @@ export function createRequestHandler(deps) {
1170
1277
  reasoning_effort: cur.reasoning_effort,
1171
1278
  };
1172
1279
  const override = stored[opt.id];
1173
- return override ? { ...opt, currentValue: override } : opt;
1280
+ return override && "options" in opt
1281
+ ? { ...opt, currentValue: override }
1282
+ : opt;
1174
1283
  });
1175
1284
  sseManager.broadcast({
1176
1285
  type: "config_option_update",
@@ -1237,7 +1346,9 @@ export function createRequestHandler(deps) {
1237
1346
  reasoning_effort: freshSession.reasoning_effort,
1238
1347
  };
1239
1348
  const override = stored[opt.id];
1240
- return override ? { ...opt, currentValue: override } : opt;
1349
+ return override && "options" in opt
1350
+ ? { ...opt, currentValue: override }
1351
+ : opt;
1241
1352
  });
1242
1353
  return opts;
1243
1354
  })()
@@ -1885,21 +1996,27 @@ export function createRequestHandler(deps) {
1885
1996
  else {
1886
1997
  sessionIdPatch = null;
1887
1998
  }
1888
- if (deps.pushService) {
1889
- const { becameVisibleForSession } = deps.pushService.updateClient(clientId, {
1999
+ if (deps.clientRegistry) {
2000
+ // setVisibility no-ops on unknown clients; auto-register here so
2001
+ // SSE-only clients (which haven't sent /hello yet) still take
2002
+ // effect. The trust boundary above already gated on identity.
2003
+ if (!deps.clientRegistry.get(clientId)) {
2004
+ deps.clientRegistry.register(clientId, { capabilities: [] });
2005
+ }
2006
+ const { becameVisibleFor } = deps.clientRegistry.setVisibility(clientId, {
1890
2007
  visible: body.visible,
1891
- sessionId: sessionIdPatch,
2008
+ active: sessionIdPatch,
1892
2009
  });
1893
2010
  // Edge-triggered only: heartbeat refreshes repeat the same
1894
2011
  // (visible:true, sessionId:X) POST every 15s — firing sendClose
1895
2012
  // on each would hammer banner recall. Only the first such
1896
2013
  // transition after a change should recall stale banners.
1897
- if (becameVisibleForSession) {
1898
- void deps.pushService.sendClose(`sess-${becameVisibleForSession}-done`);
2014
+ if (becameVisibleFor && deps.pushService) {
2015
+ void deps.pushService.sendClose(`sess-${becameVisibleFor}-done`);
1899
2016
  if (sessions) {
1900
2017
  for (const perm of sessions.pendingPermissions.values()) {
1901
- if (perm.sessionId === becameVisibleForSession) {
1902
- void deps.pushService.sendClose(`sess-${becameVisibleForSession}-perm-${perm.requestId}`);
2018
+ if (perm.sessionId === becameVisibleFor) {
2019
+ void deps.pushService.sendClose(`sess-${becameVisibleFor}-perm-${perm.requestId}`);
1903
2020
  }
1904
2021
  }
1905
2022
  }
@@ -2014,7 +2131,11 @@ export function createRequestHandler(deps) {
2014
2131
  const ext = extname(filePath);
2015
2132
  const base = filePath.slice(filePath.lastIndexOf("/") + 1);
2016
2133
  const isHashedAsset = /\.[A-Za-z0-9_-]{8,}\.(js|css)$/.test(base);
2017
- const cacheControl = isHashedAsset
2134
+ // `/lib/**` is vendored third-party content pinned by upstream SHA in
2135
+ // package metadata — treat as immutable just like content-hashed bundles.
2136
+ // Avoids re-downloading multi-hundred-KB wasm on every consumer init.
2137
+ const isVendoredLib = staticPath.startsWith("/lib/");
2138
+ const cacheControl = isHashedAsset || isVendoredLib
2018
2139
  ? "public, max-age=31536000, immutable"
2019
2140
  : "no-cache";
2020
2141
  const headers = {
package/lib/server.js CHANGED
@@ -22,6 +22,7 @@ import { join as pathJoin } from "node:path";
22
22
  import { resolveSessionsAnchor } from "./sessions-anchor.js";
23
23
  import { runStartupChecks } from "./startup-checks.js";
24
24
  import { AttachmentDispatcher } from "./attachment-dispatch.js";
25
+ import { buildBridgeEventHandlerConfig as _buildBridgeEventHandlerConfig } from "./bridge-event-config.js";
25
26
  import { createCounters as createAttachmentInterceptorCounters, } from "./attachment-interceptor.js";
26
27
  // Prefix all console output with ISO-ish timestamps (YYYY-MM-DD HH:MM:SS)
27
28
  for (const method of ["log", "error", "warn"]) {
@@ -75,13 +76,14 @@ setInterval(() => {
75
76
  .info("counters", { ...attachmentInterceptorCounters });
76
77
  }, ATTACHMENT_INTERCEPTOR_DUMP_MS).unref();
77
78
  const sessions = new SessionManager(store, config.default_cwd, config.data_dir);
78
- const titleService = new TitleService(store, sessions, config.default_cwd, config.title.model);
79
+ const titleService = new TitleService(store, sessions, config.default_cwd, config.title.models);
80
+ const sseManager = new SseManager();
81
+ const clientRegistry = new ClientRegistry();
79
82
  const pushService = new PushService(store, config.data_dir, config.push.vapid_subject, {
80
83
  globalVisibilitySuppression: config.push.global_visibility_suppression,
84
+ clientRegistry,
81
85
  });
82
86
  console.log(`[push] VAPID public key ready`);
83
- const sseManager = new SseManager();
84
- const clientRegistry = new ClientRegistry();
85
87
  sseManager.onRemove((clientId) => {
86
88
  pushService.removeClient(clientId);
87
89
  clientRegistry.remove(clientId);
@@ -128,26 +130,20 @@ const server = createServer((req, res) => {
128
130
  async function initBridge(agentCmd) {
129
131
  const b = new AgentBridge(agentCmd);
130
132
  b.setAttachmentDispatcher(attachmentDispatcher);
133
+ const eventHandlerConfig = _buildBridgeEventHandlerConfig({
134
+ cancelTimeout: config.limits.cancel_timeout,
135
+ recentPathsLimit: config.limits.recent_paths,
136
+ attachmentInterceptorCounters,
137
+ shouldLogSchemaDrift: () => {
138
+ const now = Date.now();
139
+ if (now - lastSchemaDriftAt < SCHEMA_DRIFT_THROTTLE_MS)
140
+ return false;
141
+ lastSchemaDriftAt = now;
142
+ return true;
143
+ },
144
+ });
131
145
  b.on("event", (event) => {
132
- handleAgentEvent(event, sessions, store, b, {
133
- cancelTimeout: config.limits.cancel_timeout,
134
- recentPathsLimit: config.limits.recent_paths,
135
- attachmentInterceptor: {
136
- counters: attachmentInterceptorCounters,
137
- logger: log.scope("attachment-interceptor"),
138
- onSchemaDrift: (ctx) => {
139
- const now = Date.now();
140
- if (now - lastSchemaDriftAt < SCHEMA_DRIFT_THROTTLE_MS)
141
- return;
142
- lastSchemaDriftAt = now;
143
- log
144
- .scope("attachment-interceptor")
145
- .error("schema drift detected — rawInput has no known path key", {
146
- ctx,
147
- });
148
- },
149
- },
150
- }, sseManager, pushService, clientRegistry);
146
+ handleAgentEvent(event, sessions, store, b, eventHandlerConfig, sseManager, pushService, clientRegistry);
151
147
  });
152
148
  await b.start();
153
149
  bridge = b;
@@ -181,7 +177,7 @@ process.on("SIGHUP", () => {
181
177
  });
182
178
  });
183
179
  // --- Start ---
184
- server.listen(config.port, "0.0.0.0", () => {
180
+ server.listen(config.port, config.host, () => {
185
181
  void (async () => {
186
182
  // The auth gate already ran in runStartupChecks (above) — either in
187
183
  // this process or in a parent that handed off via WEBAGENT_STARTUP_
@@ -236,7 +236,7 @@ export class SessionManager {
236
236
  };
237
237
  return configOptions.map((opt) => {
238
238
  const override = stored[opt.id];
239
- if (override)
239
+ if (override && "options" in opt)
240
240
  return { ...opt, currentValue: override };
241
241
  return opt;
242
242
  });
@@ -44,6 +44,7 @@ export async function runStartupChecks(config) {
44
44
  data_dir: config.data_dir,
45
45
  agent_cmd: config.agent_cmd,
46
46
  port: config.port,
47
+ host: config.host,
47
48
  });
48
49
  // 2. Auth bootstrap. Same `[check] auth: ...` style; first-run mint
49
50
  // or refuse-to-serve land here.
package/lib/store.js CHANGED
@@ -214,10 +214,22 @@ export class Store {
214
214
  size INTEGER NOT NULL,
215
215
  realpath TEXT NOT NULL,
216
216
  upload_seq INTEGER NOT NULL,
217
+ width INTEGER,
218
+ height INTEGER,
217
219
  created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now'))
218
220
  );
219
221
  CREATE INDEX IF NOT EXISTS idx_attachments_session ON attachments(session_id);
220
222
  `);
223
+ const attachmentCols = this.db
224
+ .prepare("PRAGMA table_info(attachments)")
225
+ .all();
226
+ const attachmentColNames = new Set(attachmentCols.map((c) => c.name));
227
+ if (!attachmentColNames.has("width")) {
228
+ this.db.exec("ALTER TABLE attachments ADD COLUMN width INTEGER");
229
+ }
230
+ if (!attachmentColNames.has("height")) {
231
+ this.db.exec("ALTER TABLE attachments ADD COLUMN height INTEGER");
232
+ }
221
233
  // owner_prefs — key-value store for owner-scoped defaults (display_name,
222
234
  // last /by selection, etc). Single-user model = single owner scope.
223
235
  // Stored as plain key/value so we don't grow a new table per pref.
@@ -618,9 +630,9 @@ export class Store {
618
630
  const uploadSeq = seqRow.s;
619
631
  this.db
620
632
  .prepare(`INSERT INTO attachments
621
- (id, session_id, kind, name, mime, size, realpath, upload_seq)
622
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
623
- .run(input.id, input.sessionId, input.kind, input.name, input.mime, input.size, input.realpath, uploadSeq);
633
+ (id, session_id, kind, name, mime, size, realpath, upload_seq, width, height)
634
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
635
+ .run(input.id, input.sessionId, input.kind, input.name, input.mime, input.size, input.realpath, uploadSeq, input.width ?? null, input.height ?? null);
624
636
  return this.db
625
637
  .prepare("SELECT * FROM attachments WHERE id = ?")
626
638
  .get(input.id);
@@ -1,4 +1,5 @@
1
1
  import { log } from "./log.js";
2
+ import { pickModelByPatterns } from "./model-picker.js";
2
3
  const tlog = log.scope("title");
3
4
  export class TitleService {
4
5
  titleSessionId = null;
@@ -12,10 +13,7 @@ export class TitleService {
12
13
  this.store = store;
13
14
  this.sessions = sessions;
14
15
  this.defaultCwd = defaultCwd;
15
- // Lowercase + drop empty strings for case-insensitive substring match.
16
- this.modelPatterns = modelPatterns
17
- .map((p) => p.trim().toLowerCase())
18
- .filter((p) => p.length > 0);
16
+ this.modelPatterns = modelPatterns;
19
17
  }
20
18
  /** Generate a title for the session (non-blocking, fire-and-forget). */
21
19
  generate(bridge, userMessage, sessionId, onTitle) {
@@ -83,7 +81,7 @@ export class TitleService {
83
81
  // the agent's reported availableModels (`configOptions[id=model].options`).
84
82
  // Empty pattern list, no model option, or no match → skip the call and
85
83
  // inherit the agent's default model (`currentModelId`).
86
- const picked = this.pickTitleModel(configOptions);
84
+ const picked = pickModelByPatterns(configOptions, this.modelPatterns);
87
85
  if (picked) {
88
86
  await bridge.setConfigOption(id, "model", picked).catch(() => []);
89
87
  }
@@ -94,18 +92,4 @@ export class TitleService {
94
92
  return null;
95
93
  }
96
94
  }
97
- /** Find the first available model whose id matches any pattern (case-insensitive). */
98
- pickTitleModel(configOptions) {
99
- if (this.modelPatterns.length === 0)
100
- return null;
101
- const modelOpt = configOptions.find((c) => c.id === "model");
102
- if (!modelOpt || modelOpt.options.length === 0)
103
- return null;
104
- for (const pattern of this.modelPatterns) {
105
- const hit = modelOpt.options.find((o) => o.value.toLowerCase().includes(pattern));
106
- if (hit)
107
- return hit.value;
108
- }
109
- return null;
110
- }
111
95
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lelouchhe/webagent",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "A terminal-style web UI for ACP-compatible agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -59,7 +59,7 @@
59
59
  ]
60
60
  },
61
61
  "dependencies": {
62
- "@agentclientprotocol/sdk": "^0.14.1",
62
+ "@agentclientprotocol/sdk": "^0.25.0",
63
63
  "@types/proper-lockfile": "^4.1.4",
64
64
  "@types/web-push": "^3.6.4",
65
65
  "better-sqlite3": "^12.6.2",
@@ -70,6 +70,7 @@
70
70
  "marked": "^18.0.2",
71
71
  "proper-lockfile": "^4.1.2",
72
72
  "smol-toml": "^1.6.0",
73
+ "temml": "^0.13.2",
73
74
  "web-push": "^3.6.7",
74
75
  "zod": "^4.3.6"
75
76
  },