llm-switcher 1.4.1 → 1.6.2
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/CHANGELOG.md +97 -0
- package/README.md +48 -0
- package/README.vi.md +45 -0
- package/docs/session-2026-10-07-compaction.md +224 -0
- package/formats.mjs +85 -25
- package/idlecache.mjs +159 -0
- package/idlecall.mjs +59 -0
- package/idlecompact.mjs +217 -0
- package/package.json +1 -1
- package/proxy.mjs +160 -7
- package/shim.mjs +29 -0
- package/state.mjs +48 -2
- package/switch.mjs +61 -0
- package/tests/certs.test.mjs +7 -1
- package/tests/codex-daemon.test.mjs +57 -3
- package/tests/compact-cli.test.mjs +34 -0
- package/tests/dashboard.e2e.test.mjs +7 -1
- package/tests/formats.test.mjs +44 -1
- package/tests/idlecompact.e2e.test.mjs +203 -0
- package/tests/idlecompact.test.mjs +282 -0
- package/ui.html +481 -18
package/ui.html
CHANGED
|
@@ -905,6 +905,10 @@
|
|
|
905
905
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 12h16M12 4v16"/><circle cx="12" cy="12" r="8"/></svg>
|
|
906
906
|
Routes & Profiles<span class="ct" id="ct-routes"></span>
|
|
907
907
|
</a>
|
|
908
|
+
<a class="nav-link" href="#/compact" data-nav="compact">
|
|
909
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 14h6m0 0v6m0-6L3 21"/><path d="M20 10h-6m0 0V4m0 6l7-7"/><circle cx="12" cy="12" r="2"/></svg>
|
|
910
|
+
Idle Compaction<span class="ct" id="ct-compact"></span>
|
|
911
|
+
</a>
|
|
908
912
|
<a class="nav-link" href="#/models" data-nav="models">
|
|
909
913
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="6" rx="1.5"/><rect x="3" y="14" width="18" height="6" rx="1.5"/></svg>
|
|
910
914
|
Model Catalog<span class="ct" id="ct-models"></span>
|
|
@@ -1016,6 +1020,186 @@
|
|
|
1016
1020
|
</div>
|
|
1017
1021
|
</div>
|
|
1018
1022
|
|
|
1023
|
+
<!-- VIEW: Idle Prefix Cache & Compaction -->
|
|
1024
|
+
<div id="view-compact" class="view-panel" style="display:none;">
|
|
1025
|
+
<div class="sec">
|
|
1026
|
+
<!-- Overview Stats -->
|
|
1027
|
+
<div class="stats">
|
|
1028
|
+
<div class="stat">
|
|
1029
|
+
<div class="k">Compaction Engine</div>
|
|
1030
|
+
<div class="v" id="stat-compact-status" style="color:var(--muted); font-size:1.4rem;">Disabled</div>
|
|
1031
|
+
<div class="status" id="stat-compact-indicator" style="margin-top:0.3rem;">Prefix caching inactive</div>
|
|
1032
|
+
</div>
|
|
1033
|
+
|
|
1034
|
+
<div class="stat">
|
|
1035
|
+
<div class="k">Inactivity Pause</div>
|
|
1036
|
+
<div class="v" id="stat-compact-pause" style="color:var(--accent); font-size:1.4rem;">15 min</div>
|
|
1037
|
+
<div class="hint" style="margin-top:0.3rem;">Trigger pause threshold</div>
|
|
1038
|
+
</div>
|
|
1039
|
+
|
|
1040
|
+
<div class="stat">
|
|
1041
|
+
<div class="k">Min Size Threshold</div>
|
|
1042
|
+
<div class="v" id="stat-compact-size" style="color:var(--mint); font-size:1.4rem;">64 KB</div>
|
|
1043
|
+
<div class="hint" style="margin-top:0.3rem;">Smaller payloads bypassed</div>
|
|
1044
|
+
</div>
|
|
1045
|
+
|
|
1046
|
+
<div class="stat">
|
|
1047
|
+
<div class="k">Cache Lookback</div>
|
|
1048
|
+
<div class="v" id="stat-compact-lookback" style="color:var(--code); font-size:1.4rem;">72 hrs</div>
|
|
1049
|
+
<div class="hint" style="margin-top:0.3rem;">Storage retention window</div>
|
|
1050
|
+
</div>
|
|
1051
|
+
</div>
|
|
1052
|
+
|
|
1053
|
+
<!-- Hero Master Card -->
|
|
1054
|
+
<div class="card pad">
|
|
1055
|
+
<div class="sech">
|
|
1056
|
+
<div>
|
|
1057
|
+
<div style="display:flex; align-items:center; gap:0.6rem;">
|
|
1058
|
+
<h2 style="font-size:1.05rem; font-weight:600;">Idle Prefix Cache & Compaction</h2>
|
|
1059
|
+
<span class="badge outline" id="badge-idle-compact">OFF</span>
|
|
1060
|
+
</div>
|
|
1061
|
+
<p class="hint" style="margin-top:0.35rem; max-width:54rem; line-height:1.45;">
|
|
1062
|
+
Shortens long conversation histories when an agent session remains inactive past a configured pause.
|
|
1063
|
+
The edge gateway maintains a canonical shortened prefix for resumed turns, cutting context tokens while keeping client transcripts 100% untouched.
|
|
1064
|
+
</p>
|
|
1065
|
+
</div>
|
|
1066
|
+
<div style="display:flex; align-items:center; gap:0.8rem;">
|
|
1067
|
+
<label class="switch" title="Toggle Idle Compaction">
|
|
1068
|
+
<input type="checkbox" id="idle-compact-enabled" onchange="saveIdleCompact()">
|
|
1069
|
+
<span></span>
|
|
1070
|
+
</label>
|
|
1071
|
+
</div>
|
|
1072
|
+
</div>
|
|
1073
|
+
<div id="idle-compact-status" style="margin-top:0.75rem; font-size:0.82rem; min-height:1.2rem;" aria-live="polite"></div>
|
|
1074
|
+
</div>
|
|
1075
|
+
|
|
1076
|
+
<!-- Configuration Section (Hidden when inactive) -->
|
|
1077
|
+
<div id="idle-compact-config-section" style="display:none; flex-direction:column; gap:1.25rem;">
|
|
1078
|
+
<!-- Card 1: Core Policy & Trigger Thresholds -->
|
|
1079
|
+
<div class="card">
|
|
1080
|
+
<div class="cardh">
|
|
1081
|
+
<h3 style="font-size:0.95rem; font-weight:600;">Compaction Policy & Triggers</h3>
|
|
1082
|
+
<span class="badge ok">Gateway Invariant</span>
|
|
1083
|
+
</div>
|
|
1084
|
+
|
|
1085
|
+
<div class="row">
|
|
1086
|
+
<div class="who">
|
|
1087
|
+
<div class="l">Inactivity Pause Threshold</div>
|
|
1088
|
+
<div class="meta">Elapsed idle minutes after which the conversation prefix is compacted</div>
|
|
1089
|
+
</div>
|
|
1090
|
+
<div style="display:flex; align-items:center; gap:0.45rem; margin-left:auto;">
|
|
1091
|
+
<input type="number" id="idle-compact-minutes" min="1" max="1440" step="any" onchange="saveIdleCompact()" style="width:7.5rem; text-align:right;" title="Inactivity pause threshold in minutes (1 – 1440 min)" placeholder="15">
|
|
1092
|
+
<span style="font-size:0.8rem; color:var(--muted); min-width:2.2rem; user-select:none;">min</span>
|
|
1093
|
+
</div>
|
|
1094
|
+
</div>
|
|
1095
|
+
|
|
1096
|
+
<div class="row">
|
|
1097
|
+
<div class="who">
|
|
1098
|
+
<div class="l">Minimum Context Payload</div>
|
|
1099
|
+
<div class="meta">Context threshold; conversations smaller than this are kept untouched</div>
|
|
1100
|
+
</div>
|
|
1101
|
+
<div style="display:flex; align-items:center; gap:0.45rem; margin-left:auto;">
|
|
1102
|
+
<input type="number" id="idle-compact-kb" min="8" max="4096" step="any" onchange="saveIdleCompact()" style="width:7.5rem; text-align:right;" title="Minimum context payload size in kilobytes (8 – 4096 KB)" placeholder="64">
|
|
1103
|
+
<span style="font-size:0.8rem; color:var(--muted); min-width:2.2rem; user-select:none;">KB</span>
|
|
1104
|
+
</div>
|
|
1105
|
+
</div>
|
|
1106
|
+
|
|
1107
|
+
<div class="row">
|
|
1108
|
+
<div class="who">
|
|
1109
|
+
<div class="l">Summarization Model</div>
|
|
1110
|
+
<div class="meta">Upstream model ID to generate turn summaries (empty = deterministic truncation)</div>
|
|
1111
|
+
</div>
|
|
1112
|
+
<div style="min-width:16rem; flex:1; max-width:26rem; margin-left:auto;">
|
|
1113
|
+
<input type="text" id="idle-compact-model" placeholder="empty = no model call (deterministic)" maxlength="512" onchange="saveIdleCompact()" style="font-family:var(--font-mono); font-size:0.8rem;" title="Upstream model used to generate turn summary text">
|
|
1114
|
+
</div>
|
|
1115
|
+
</div>
|
|
1116
|
+
|
|
1117
|
+
<div class="row">
|
|
1118
|
+
<div class="who">
|
|
1119
|
+
<div class="l">Shorten Codex CLI Sessions</div>
|
|
1120
|
+
<div class="meta">Off by default; Codex retains native turn history in local SQLite storage</div>
|
|
1121
|
+
</div>
|
|
1122
|
+
<label class="switch" title="Toggle Codex compaction" style="margin-left:auto;">
|
|
1123
|
+
<input type="checkbox" id="idle-compact-codex" onchange="saveIdleCompact()">
|
|
1124
|
+
<span></span>
|
|
1125
|
+
</label>
|
|
1126
|
+
</div>
|
|
1127
|
+
</div>
|
|
1128
|
+
|
|
1129
|
+
<!-- Card 2: Context Retention & Budget Controls -->
|
|
1130
|
+
<div class="card">
|
|
1131
|
+
<div class="cardh">
|
|
1132
|
+
<h3 style="font-size:0.95rem; font-weight:600;">Context Retention & Cache Budgets</h3>
|
|
1133
|
+
<span class="badge accent">Turn Budgets</span>
|
|
1134
|
+
</div>
|
|
1135
|
+
|
|
1136
|
+
<div class="row">
|
|
1137
|
+
<div class="who">
|
|
1138
|
+
<div class="l">Verbatim Recent Turns</div>
|
|
1139
|
+
<div class="meta">Number of most recent turns always preserved byte-for-byte</div>
|
|
1140
|
+
</div>
|
|
1141
|
+
<div style="display:flex; align-items:center; gap:0.45rem; margin-left:auto;">
|
|
1142
|
+
<input type="number" id="idle-compact-keep" min="1" max="100" onchange="saveIdleCompact()" style="width:7.5rem; text-align:right;" title="Number of most recent turns to retain verbatim (1 – 100 turns)" placeholder="6">
|
|
1143
|
+
<span style="font-size:0.8rem; color:var(--muted); min-width:2.4rem; user-select:none;">turns</span>
|
|
1144
|
+
</div>
|
|
1145
|
+
</div>
|
|
1146
|
+
|
|
1147
|
+
<div class="row">
|
|
1148
|
+
<div class="who">
|
|
1149
|
+
<div class="l">Character Budget Per Earlier Turn</div>
|
|
1150
|
+
<div class="meta">Maximum character budget preserved per earlier conversation turn</div>
|
|
1151
|
+
</div>
|
|
1152
|
+
<div style="display:flex; align-items:center; gap:0.45rem; margin-left:auto;">
|
|
1153
|
+
<input type="number" id="idle-compact-user-chars" min="128" max="24000" onchange="saveIdleCompact()" style="width:7.5rem; text-align:right;" title="Character budget limit per earlier text turn (128 – 24000 chars)" placeholder="3000">
|
|
1154
|
+
<span style="font-size:0.8rem; color:var(--muted); min-width:2.4rem; user-select:none;">chars</span>
|
|
1155
|
+
</div>
|
|
1156
|
+
</div>
|
|
1157
|
+
|
|
1158
|
+
<div class="row">
|
|
1159
|
+
<div class="who">
|
|
1160
|
+
<div class="l">Maximum Summary Length</div>
|
|
1161
|
+
<div class="meta">Upper character limit allocated for generated turn summary blocks</div>
|
|
1162
|
+
</div>
|
|
1163
|
+
<div style="display:flex; align-items:center; gap:0.45rem; margin-left:auto;">
|
|
1164
|
+
<input type="number" id="idle-compact-summary-chars" min="256" max="128000" onchange="saveIdleCompact()" style="width:7.5rem; text-align:right;" title="Upper character limit allocated for generated summary (256 – 128000 chars)" placeholder="24000">
|
|
1165
|
+
<span style="font-size:0.8rem; color:var(--muted); min-width:2.4rem; user-select:none;">chars</span>
|
|
1166
|
+
</div>
|
|
1167
|
+
</div>
|
|
1168
|
+
|
|
1169
|
+
<div class="row">
|
|
1170
|
+
<div class="who">
|
|
1171
|
+
<div class="l">Cache Prefix Retention Window</div>
|
|
1172
|
+
<div class="meta">Duration in hours before idle cached prefixes are pruned from state disk</div>
|
|
1173
|
+
</div>
|
|
1174
|
+
<div style="display:flex; align-items:center; gap:0.45rem; margin-left:auto;">
|
|
1175
|
+
<input type="number" id="idle-compact-lookback" min="1" max="168" step="any" onchange="saveIdleCompact()" style="width:7.5rem; text-align:right;" title="Duration in hours before idle cached prefixes are pruned (1 – 168 hours)" placeholder="72">
|
|
1176
|
+
<span style="font-size:0.8rem; color:var(--muted); min-width:2.4rem; user-select:none;">hours</span>
|
|
1177
|
+
</div>
|
|
1178
|
+
</div>
|
|
1179
|
+
</div>
|
|
1180
|
+
|
|
1181
|
+
<!-- Card 3: Architecture & Safety Invariants -->
|
|
1182
|
+
<div class="card pad">
|
|
1183
|
+
<h3 style="font-size:0.95rem; font-weight:600; margin-bottom:0.75rem;">Edge Gateway Invariants & Storage Safety</h3>
|
|
1184
|
+
<div class="sec">
|
|
1185
|
+
<div class="copyline">
|
|
1186
|
+
<code>Zero Client Mutation: Transcripts on client disk (Claude Code / Codex) are never modified.</code>
|
|
1187
|
+
</div>
|
|
1188
|
+
<div class="copyline">
|
|
1189
|
+
<code>Atomic CAS Cache: Compare-and-swap generation counters prevent stale compaction races.</code>
|
|
1190
|
+
</div>
|
|
1191
|
+
<div class="copyline">
|
|
1192
|
+
<code>Private State Storage: Cached prefixes saved under ~/.llm-switcher/state/idle-compact/ (0600 permissions).</code>
|
|
1193
|
+
</div>
|
|
1194
|
+
<div class="copyline">
|
|
1195
|
+
<code>Deterministic Safety: If no summary model is configured, gateway uses pure heuristic truncation without external calls.</code>
|
|
1196
|
+
</div>
|
|
1197
|
+
</div>
|
|
1198
|
+
</div>
|
|
1199
|
+
</div>
|
|
1200
|
+
</div>
|
|
1201
|
+
</div>
|
|
1202
|
+
|
|
1019
1203
|
<!-- VIEW 2: Discovered Models Catalog -->
|
|
1020
1204
|
<div id="view-models" class="view-panel" style="display:none;">
|
|
1021
1205
|
<div class="sec">
|
|
@@ -1191,13 +1375,6 @@
|
|
|
1191
1375
|
</div>
|
|
1192
1376
|
|
|
1193
1377
|
<div style="display:grid; grid-template-columns:1fr 1fr; gap:0.9rem;">
|
|
1194
|
-
<label class="f">Thinking Mode
|
|
1195
|
-
<select id="p-thinking-mode">
|
|
1196
|
-
<option value="auto">AUTO (restores reasoning, strips tags safely)</option>
|
|
1197
|
-
<option value="native">NATIVE (strict reasoning_effort)</option>
|
|
1198
|
-
<option value="off">OFF (never send thinking)</option>
|
|
1199
|
-
</select>
|
|
1200
|
-
</label>
|
|
1201
1378
|
<label class="f">Routing Mode
|
|
1202
1379
|
<select id="p-mode">
|
|
1203
1380
|
<option value="convert">CONVERT (Bi-directional translation)</option>
|
|
@@ -1211,11 +1388,46 @@
|
|
|
1211
1388
|
<!-- TAB 3: Models -->
|
|
1212
1389
|
<div id="tab-models" class="tab-content" style="display:none; gap:0.9rem; flex-direction:column;">
|
|
1213
1390
|
<div style="display:flex; justify-content:space-between; align-items:center; gap:0.75rem;">
|
|
1214
|
-
<
|
|
1391
|
+
<div>
|
|
1392
|
+
<h3 id="model-slot-heading" style="font-size:0.92rem; font-weight:600; margin:0;">Model Slots Mapping</h3>
|
|
1393
|
+
<span class="hint" style="font-size:0.75rem;">Configure target model and individual thinking effort for each slot</span>
|
|
1394
|
+
</div>
|
|
1215
1395
|
<div id="models-status" class="models-status" aria-live="polite"></div>
|
|
1216
1396
|
</div>
|
|
1217
1397
|
|
|
1218
1398
|
<div id="slot-fields" style="display:grid; gap:0.75rem;"></div>
|
|
1399
|
+
|
|
1400
|
+
<!-- Global Reasoning Policy -->
|
|
1401
|
+
<div style="border-top:1px solid var(--line); padding-top:0.85rem; margin-top:0.35rem; display:grid; gap:0.85rem;">
|
|
1402
|
+
<div style="display:flex; justify-content:space-between; align-items:center;">
|
|
1403
|
+
<h4 style="font-size:0.88rem; font-weight:600; margin:0;">Reasoning Mode & Policy</h4>
|
|
1404
|
+
<span class="hint">Controls reasoning behavior across all model slots</span>
|
|
1405
|
+
</div>
|
|
1406
|
+
|
|
1407
|
+
<div style="display:grid; grid-template-columns:1fr 1fr; gap:0.9rem;">
|
|
1408
|
+
<label class="f">Thinking Mode
|
|
1409
|
+
<select id="p-thinking-mode">
|
|
1410
|
+
<option value="auto">AUTO (restores reasoning, strips tags safely)</option>
|
|
1411
|
+
<option value="native">NATIVE (strict reasoning_effort)</option>
|
|
1412
|
+
<option value="off">OFF (never send thinking to any model)</option>
|
|
1413
|
+
</select>
|
|
1414
|
+
</label>
|
|
1415
|
+
<label class="f" id="f-thinking-effort" style="display:none;">Global Fallback Effort
|
|
1416
|
+
<select id="p-thinking-effort">
|
|
1417
|
+
<option value="auto">Auto (inherit from client tool)</option>
|
|
1418
|
+
<option value="low">Low (fast, light reasoning)</option>
|
|
1419
|
+
<option value="medium">Medium (balanced reasoning)</option>
|
|
1420
|
+
<option value="high">High (deep reasoning)</option>
|
|
1421
|
+
<option value="xhigh">Extra High (complex problem solving)</option>
|
|
1422
|
+
<option value="max">Max (maximum reasoning depth)</option>
|
|
1423
|
+
</select>
|
|
1424
|
+
</label>
|
|
1425
|
+
</div>
|
|
1426
|
+
|
|
1427
|
+
<div id="bifrost-hint" style="display:none; padding:0.6rem 0.85rem; border-radius:6px; background:rgba(59,130,246,0.08); border:1px solid rgba(59,130,246,0.25); font-size:0.82rem; color:#60a5fa; line-height:1.4;">
|
|
1428
|
+
⚡ <strong>Bifrost Passthrough:</strong> Byte-for-byte routing to intact without format conversion. Thinking effort is controlled natively by your client tool.
|
|
1429
|
+
</div>
|
|
1430
|
+
</div>
|
|
1219
1431
|
</div>
|
|
1220
1432
|
|
|
1221
1433
|
<!-- Test Connection Box -->
|
|
@@ -1423,7 +1635,7 @@
|
|
|
1423
1635
|
function handleRoute() {
|
|
1424
1636
|
const hash = window.location.hash || '#/routes';
|
|
1425
1637
|
const route = hash.replace(/^#\/?/, '').split('/')[0] || 'routes';
|
|
1426
|
-
const validRoutes = ['routes', 'models', 'logs', 'doctor'];
|
|
1638
|
+
const validRoutes = ['routes', 'compact', 'models', 'logs', 'doctor'];
|
|
1427
1639
|
const activeRoute = validRoutes.includes(route) ? route : 'routes';
|
|
1428
1640
|
|
|
1429
1641
|
// Update active nav link
|
|
@@ -1444,6 +1656,7 @@
|
|
|
1444
1656
|
updateHeader(activeRoute);
|
|
1445
1657
|
|
|
1446
1658
|
// Lazy load view content
|
|
1659
|
+
if (activeRoute === 'compact') loadIdleCompact();
|
|
1447
1660
|
if (activeRoute === 'models') renderCatalogView();
|
|
1448
1661
|
if (activeRoute === 'logs') loadInspectorLogs();
|
|
1449
1662
|
}
|
|
@@ -1476,6 +1689,14 @@
|
|
|
1476
1689
|
<button class="btn btn-outline" id="btn-toggle-routes" onclick="toggleProxy()">${activeTargets.length > 0 ? 'Use official endpoints' : 'Activate compatible routes'}</button>
|
|
1477
1690
|
<button class="btn btn-outline" onclick="loadStatus()">Refresh</button>
|
|
1478
1691
|
`;
|
|
1692
|
+
} else if (route === 'compact') {
|
|
1693
|
+
loadIdleCompact();
|
|
1694
|
+
titleEl.textContent = 'Idle Compaction';
|
|
1695
|
+
subEl.textContent = 'Edge gateway prefix compaction and cache pruning for long sessions.';
|
|
1696
|
+
actionsEl.innerHTML = `
|
|
1697
|
+
<button class="btn btn-outline" onclick="loadIdleCompact()">Refresh</button>
|
|
1698
|
+
<button class="btn btn-primary" onclick="saveIdleCompact()">Save Configuration</button>
|
|
1699
|
+
`;
|
|
1479
1700
|
} else if (route === 'models') {
|
|
1480
1701
|
titleEl.textContent = 'Dynamic Model Catalog';
|
|
1481
1702
|
subEl.textContent = 'Auto-polled models from upstream providers to guarantee zero 404/drift errors.';
|
|
@@ -1623,6 +1844,11 @@
|
|
|
1623
1844
|
return defaultModels[slotId] || '';
|
|
1624
1845
|
}
|
|
1625
1846
|
|
|
1847
|
+
function effortForSlot(modelEfforts, slotId) {
|
|
1848
|
+
if (!modelEfforts) return 'auto';
|
|
1849
|
+
return modelEfforts[slotId] || 'auto';
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1626
1852
|
function renderModelSlots(slotFmt, p) {
|
|
1627
1853
|
const container = document.getElementById('slot-fields');
|
|
1628
1854
|
const heading = document.getElementById('model-slot-heading');
|
|
@@ -1635,11 +1861,27 @@
|
|
|
1635
1861
|
|
|
1636
1862
|
for (const s of cfg.slots) {
|
|
1637
1863
|
const val = currentProfileBuffer ? valueForSlot(currentProfileBuffer.defaultModels, s.id) : valueForSlot(p?.defaultModels, s.id);
|
|
1864
|
+
const effVal = currentProfileBuffer ? effortForSlot(currentProfileBuffer.modelEfforts, s.id) : effortForSlot(p?.modelEfforts, s.id);
|
|
1638
1865
|
|
|
1639
1866
|
const slotRow = document.createElement('div');
|
|
1640
|
-
slotRow.
|
|
1867
|
+
slotRow.className = 'slot-row-card';
|
|
1868
|
+
slotRow.style.cssText = 'display:flex; flex-direction:column; gap:0.35rem; padding:0.6rem 0.75rem; border-radius:6px; background:var(--card-bg, rgba(255,255,255,0.02)); border:1px solid var(--line);';
|
|
1641
1869
|
slotRow.innerHTML = `
|
|
1642
|
-
<
|
|
1870
|
+
<div style="display:flex; justify-content:space-between; align-items:center; gap:0.5rem;">
|
|
1871
|
+
<label for="p-${s.id}" style="font-size:0.82rem; font-weight:600; margin:0;">${escapeHtml(s.name)}</label>
|
|
1872
|
+
<div style="display:flex; align-items:center; gap:0.35rem;">
|
|
1873
|
+
<label for="p-effort-${s.id}" style="font-size:0.75rem; color:var(--muted); margin:0;">Target Effort:</label>
|
|
1874
|
+
<select id="p-effort-${s.id}" class="slot-effort inp" onchange="formDirty = true" style="height:26px; padding:0 0.4rem; font-size:0.78rem; border-radius:4px; min-width:6.8rem;">
|
|
1875
|
+
<option value="auto">Auto (Default)</option>
|
|
1876
|
+
<option value="none">None (0)</option>
|
|
1877
|
+
<option value="low">Low</option>
|
|
1878
|
+
<option value="medium">Medium</option>
|
|
1879
|
+
<option value="high">High</option>
|
|
1880
|
+
<option value="xhigh">Extra High</option>
|
|
1881
|
+
<option value="max">Max</option>
|
|
1882
|
+
</select>
|
|
1883
|
+
</div>
|
|
1884
|
+
</div>
|
|
1643
1885
|
<div class="combo">
|
|
1644
1886
|
<input type="text" id="p-${s.id}" oninput="updateWindowNotes()" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="p-${s.id}-list" autocomplete="off" spellcheck="false" placeholder="${escapeHtml(s.placeholder)}" value="${escapeHtml(val)}">
|
|
1645
1887
|
<button type="button" class="combo-toggle" tabindex="-1" aria-label="Show models for ${escapeHtml(s.name)}">
|
|
@@ -1647,10 +1889,15 @@
|
|
|
1647
1889
|
</button>
|
|
1648
1890
|
<ul class="combo-list" id="p-${s.id}-list" role="listbox" aria-label="${escapeHtml(s.name)} models" hidden></ul>
|
|
1649
1891
|
</div>
|
|
1650
|
-
<div
|
|
1892
|
+
<div style="display:flex; justify-content:space-between; align-items:center; gap:0.5rem; min-height:1rem;">
|
|
1893
|
+
<div class="slot-note" id="p-${s.id}-note" role="status" aria-live="polite"></div>
|
|
1894
|
+
<div class="slot-bifrost-hint" id="p-bifrost-${s.id}" style="display:none; font-size:0.74rem; color:#60a5fa;">⚡ Bifrost Passthrough (native effort)</div>
|
|
1895
|
+
</div>
|
|
1651
1896
|
`;
|
|
1652
1897
|
container.appendChild(slotRow);
|
|
1653
1898
|
wireCombo(slotRow.querySelector('.combo'));
|
|
1899
|
+
const effSelect = slotRow.querySelector('#p-effort-' + s.id);
|
|
1900
|
+
if (effSelect) effSelect.value = effVal || 'auto';
|
|
1654
1901
|
}
|
|
1655
1902
|
updateWindowNotes();
|
|
1656
1903
|
}
|
|
@@ -1664,6 +1911,7 @@
|
|
|
1664
1911
|
const cfg = getActiveSlotConfig(renderedSlotFormat);
|
|
1665
1912
|
if (!currentProfileBuffer) currentProfileBuffer = {};
|
|
1666
1913
|
const defaultModels = { ...(currentProfileBuffer.defaultModels || {}) };
|
|
1914
|
+
const modelEfforts = { ...(currentProfileBuffer.modelEfforts || {}) };
|
|
1667
1915
|
for (const s of cfg.slots) {
|
|
1668
1916
|
const inp = document.getElementById('p-' + s.id);
|
|
1669
1917
|
if (inp) {
|
|
@@ -1671,13 +1919,19 @@
|
|
|
1671
1919
|
if (val) defaultModels[s.id] = val;
|
|
1672
1920
|
else delete defaultModels[s.id];
|
|
1673
1921
|
}
|
|
1922
|
+
const effSelect = document.getElementById('p-effort-' + s.id);
|
|
1923
|
+
if (effSelect) {
|
|
1924
|
+
const effVal = effSelect.value;
|
|
1925
|
+
if (effVal && effVal !== 'auto') modelEfforts[s.id] = effVal;
|
|
1926
|
+
else delete modelEfforts[s.id];
|
|
1927
|
+
}
|
|
1674
1928
|
}
|
|
1675
1929
|
currentProfileBuffer.defaultModels = defaultModels;
|
|
1930
|
+
currentProfileBuffer.modelEfforts = modelEfforts;
|
|
1676
1931
|
// An old config can still carry the retired 1M flags. They are dropped on the next save
|
|
1677
1932
|
// instead of being written back: the window is the model's own now.
|
|
1678
1933
|
delete currentProfileBuffer.model1M;
|
|
1679
|
-
return { defaultModels };
|
|
1680
|
-
|
|
1934
|
+
return { defaultModels, modelEfforts };
|
|
1681
1935
|
}
|
|
1682
1936
|
|
|
1683
1937
|
// 128000 reads as 128K and 262144 as 256K: a round decimal count keeps base 1000.
|
|
@@ -1704,6 +1958,64 @@
|
|
|
1704
1958
|
const model = document.getElementById('p-' + s.id)?.value.trim() || '';
|
|
1705
1959
|
note.textContent = model ? windowLine(model) : '';
|
|
1706
1960
|
}
|
|
1961
|
+
syncEffortOptions();
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
function syncEffortOptions() {
|
|
1965
|
+
const mode = document.getElementById('p-thinking-mode')?.value;
|
|
1966
|
+
const url = (document.getElementById('p-url')?.value || '').toLowerCase();
|
|
1967
|
+
const tool = document.getElementById('p-tool')?.value || 'auto';
|
|
1968
|
+
const isIntact = url.includes('intact') || url.includes(':20130') || url.includes(':8080') ||
|
|
1969
|
+
url.includes(':20142') || url.includes(':20150') || url.includes(':20152');
|
|
1970
|
+
const isThinkingOff = mode === 'off';
|
|
1971
|
+
|
|
1972
|
+
const cfg = getActiveSlotConfig(renderedSlotFormat);
|
|
1973
|
+
let anyBifrost = false;
|
|
1974
|
+
|
|
1975
|
+
for (const s of cfg.slots) {
|
|
1976
|
+
const inp = document.getElementById('p-' + s.id);
|
|
1977
|
+
const effSelect = document.getElementById('p-effort-' + s.id);
|
|
1978
|
+
const bifrostHint = document.getElementById('p-bifrost-' + s.id);
|
|
1979
|
+
if (!effSelect) continue;
|
|
1980
|
+
|
|
1981
|
+
const modelVal = (inp?.value || '').trim().toLowerCase();
|
|
1982
|
+
let slotIsBifrost = false;
|
|
1983
|
+
if (isIntact && modelVal) {
|
|
1984
|
+
if (tool === 'claude') slotIsBifrost = modelVal.startsWith('claude/') || modelVal.startsWith('anthropic/');
|
|
1985
|
+
else if (tool === 'codex') slotIsBifrost = modelVal.startsWith('codex/');
|
|
1986
|
+
else if (tool === 'agy') slotIsBifrost = modelVal.startsWith('antigravity/');
|
|
1987
|
+
else if (tool === 'auto') slotIsBifrost = modelVal.startsWith('claude/') || modelVal.startsWith('anthropic/') || modelVal.startsWith('codex/') || modelVal.startsWith('antigravity/');
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
if (slotIsBifrost) {
|
|
1991
|
+
anyBifrost = true;
|
|
1992
|
+
if (bifrostHint) bifrostHint.style.display = 'block';
|
|
1993
|
+
effSelect.disabled = true;
|
|
1994
|
+
effSelect.style.opacity = '0.6';
|
|
1995
|
+
effSelect.title = 'Bifrost passthrough active: effort controlled natively by client tool';
|
|
1996
|
+
} else {
|
|
1997
|
+
if (bifrostHint) bifrostHint.style.display = 'none';
|
|
1998
|
+
if (isThinkingOff) {
|
|
1999
|
+
effSelect.disabled = true;
|
|
2000
|
+
effSelect.style.opacity = '0.5';
|
|
2001
|
+
effSelect.title = 'Thinking mode is OFF globally';
|
|
2002
|
+
} else {
|
|
2003
|
+
effSelect.disabled = false;
|
|
2004
|
+
effSelect.style.opacity = '1';
|
|
2005
|
+
effSelect.title = 'Target thinking effort for this model';
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
const effortEl = document.getElementById('p-thinking-effort');
|
|
2011
|
+
const globalBifrostHint = document.getElementById('bifrost-hint');
|
|
2012
|
+
if (effortEl) {
|
|
2013
|
+
effortEl.disabled = isThinkingOff;
|
|
2014
|
+
effortEl.style.opacity = isThinkingOff ? '0.5' : '1';
|
|
2015
|
+
}
|
|
2016
|
+
if (globalBifrostHint) {
|
|
2017
|
+
globalBifrostHint.style.display = anyBifrost ? 'block' : 'none';
|
|
2018
|
+
}
|
|
1707
2019
|
}
|
|
1708
2020
|
|
|
1709
2021
|
// Prefetch Counts for Nav Badges
|
|
@@ -1731,9 +2043,135 @@
|
|
|
1731
2043
|
if (ctLogs && lastLogsCount > 0) ctLogs.textContent = lastLogsCount;
|
|
1732
2044
|
}
|
|
1733
2045
|
} catch {}
|
|
2046
|
+
|
|
2047
|
+
loadIdleCompact();
|
|
1734
2048
|
}
|
|
1735
2049
|
|
|
1736
2050
|
// Load Status
|
|
2051
|
+
// Idle compaction, read and written through the same endpoint `switch compact` uses, so the
|
|
2052
|
+
// dashboard and the terminal cannot show two different settings.
|
|
2053
|
+
function showIdleCompact(c) {
|
|
2054
|
+
c = c || {};
|
|
2055
|
+
const enabled = !!c.enabled;
|
|
2056
|
+
const elEnabled = document.getElementById('idle-compact-enabled');
|
|
2057
|
+
if (elEnabled) elEnabled.checked = enabled;
|
|
2058
|
+
const configSec = document.getElementById('idle-compact-config-section');
|
|
2059
|
+
if (configSec) configSec.style.display = enabled ? 'flex' : 'none';
|
|
2060
|
+
const elMinutes = document.getElementById('idle-compact-minutes');
|
|
2061
|
+
if (elMinutes) elMinutes.value = c.idleMinutes ?? 15;
|
|
2062
|
+
const elKb = document.getElementById('idle-compact-kb');
|
|
2063
|
+
if (elKb) elKb.value = (c.minBytes ?? 65536) / 1024;
|
|
2064
|
+
const elModel = document.getElementById('idle-compact-model');
|
|
2065
|
+
if (elModel) elModel.value = c.model || '';
|
|
2066
|
+
const elCodex = document.getElementById('idle-compact-codex');
|
|
2067
|
+
if (elCodex) elCodex.checked = !!c.codex;
|
|
2068
|
+
const elKeep = document.getElementById('idle-compact-keep');
|
|
2069
|
+
if (elKeep) elKeep.value = c.keepRecent ?? 6;
|
|
2070
|
+
const elUserChars = document.getElementById('idle-compact-user-chars');
|
|
2071
|
+
if (elUserChars) elUserChars.value = c.userChars ?? 3000;
|
|
2072
|
+
const elSummaryChars = document.getElementById('idle-compact-summary-chars');
|
|
2073
|
+
if (elSummaryChars) elSummaryChars.value = c.summaryMaxChars ?? 24000;
|
|
2074
|
+
const elLookback = document.getElementById('idle-compact-lookback');
|
|
2075
|
+
if (elLookback) elLookback.value = c.sessionLookbackHours ?? 72;
|
|
2076
|
+
|
|
2077
|
+
// Badge on hero card
|
|
2078
|
+
const badge = document.getElementById('badge-idle-compact');
|
|
2079
|
+
if (badge) {
|
|
2080
|
+
badge.textContent = enabled ? 'ACTIVE' : 'OFF';
|
|
2081
|
+
badge.className = 'badge ' + (enabled ? 'ok' : 'outline');
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// Nav link count
|
|
2085
|
+
const ctCompact = document.getElementById('ct-compact');
|
|
2086
|
+
if (ctCompact) {
|
|
2087
|
+
ctCompact.textContent = enabled ? 'ON' : '';
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
// Stats cards
|
|
2091
|
+
const statStatus = document.getElementById('stat-compact-status');
|
|
2092
|
+
if (statStatus) {
|
|
2093
|
+
statStatus.textContent = enabled ? 'Active' : 'Disabled';
|
|
2094
|
+
statStatus.style.color = enabled ? 'var(--ok)' : 'var(--muted)';
|
|
2095
|
+
}
|
|
2096
|
+
const statInd = document.getElementById('stat-compact-indicator');
|
|
2097
|
+
if (statInd) {
|
|
2098
|
+
statInd.textContent = enabled ? 'Prefix caching active' : 'Prefix caching inactive';
|
|
2099
|
+
statInd.className = 'status ' + (enabled ? 'on' : '');
|
|
2100
|
+
}
|
|
2101
|
+
const statPause = document.getElementById('stat-compact-pause');
|
|
2102
|
+
if (statPause) {
|
|
2103
|
+
statPause.textContent = (c.idleMinutes ?? 15) + ' min';
|
|
2104
|
+
}
|
|
2105
|
+
const statSize = document.getElementById('stat-compact-size');
|
|
2106
|
+
if (statSize) {
|
|
2107
|
+
statSize.textContent = ((c.minBytes ?? 65536) / 1024) + ' KB';
|
|
2108
|
+
}
|
|
2109
|
+
const statLookback = document.getElementById('stat-compact-lookback');
|
|
2110
|
+
if (statLookback) {
|
|
2111
|
+
statLookback.textContent = (c.sessionLookbackHours ?? 72) + ' hrs';
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
async function loadIdleCompact() {
|
|
2116
|
+
try {
|
|
2117
|
+
const r = await api('/api/idle-compact');
|
|
2118
|
+
if (r.ok) showIdleCompact((await r.json()).idleCompact);
|
|
2119
|
+
} catch { /* the card stays at its defaults; nothing here is required to route */ }
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
async function saveIdleCompact() {
|
|
2123
|
+
const isEnabled = document.getElementById('idle-compact-enabled').checked;
|
|
2124
|
+
const configSec = document.getElementById('idle-compact-config-section');
|
|
2125
|
+
if (configSec) configSec.style.display = isEnabled ? 'flex' : 'none';
|
|
2126
|
+
const status = document.getElementById('idle-compact-status');
|
|
2127
|
+
for (const input of document.querySelectorAll('[id^="idle-compact-"][type="number"]')) {
|
|
2128
|
+
if (!input.value || !input.checkValidity()) {
|
|
2129
|
+
input.reportValidity();
|
|
2130
|
+
if (status) {
|
|
2131
|
+
status.textContent = 'Not saved: enter a value within the displayed bounds.';
|
|
2132
|
+
status.style.color = 'var(--err)';
|
|
2133
|
+
}
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
const patch = {
|
|
2138
|
+
codex: document.getElementById('idle-compact-codex').checked,
|
|
2139
|
+
keepRecent: Number(document.getElementById('idle-compact-keep').value),
|
|
2140
|
+
userChars: Number(document.getElementById('idle-compact-user-chars').value),
|
|
2141
|
+
summaryMaxChars: Number(document.getElementById('idle-compact-summary-chars').value),
|
|
2142
|
+
sessionLookbackHours: Number(document.getElementById('idle-compact-lookback').value),
|
|
2143
|
+
enabled: document.getElementById('idle-compact-enabled').checked,
|
|
2144
|
+
idleMinutes: Number(document.getElementById('idle-compact-minutes').value),
|
|
2145
|
+
minBytes: Math.round(Number(document.getElementById('idle-compact-kb').value) * 1024),
|
|
2146
|
+
model: document.getElementById('idle-compact-model').value.trim()
|
|
2147
|
+
};
|
|
2148
|
+
if (status) {
|
|
2149
|
+
status.textContent = 'Saving...';
|
|
2150
|
+
status.style.color = 'var(--muted)';
|
|
2151
|
+
}
|
|
2152
|
+
try {
|
|
2153
|
+
const r = await api('/api/idle-compact', {
|
|
2154
|
+
method: 'POST',
|
|
2155
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2156
|
+
body: JSON.stringify(patch)
|
|
2157
|
+
});
|
|
2158
|
+
if (!r.ok) throw new Error((await r.json()).error || r.status);
|
|
2159
|
+
showIdleCompact((await r.json()).idleCompact);
|
|
2160
|
+
if (status) {
|
|
2161
|
+
status.textContent = 'Saved. Applies to subsequent conversations that resume after a pause.';
|
|
2162
|
+
status.style.color = 'var(--ok)';
|
|
2163
|
+
setTimeout(() => { if (status) status.textContent = ''; }, 4500);
|
|
2164
|
+
}
|
|
2165
|
+
showToast('Idle compaction configuration saved');
|
|
2166
|
+
} catch (e) {
|
|
2167
|
+
if (status) {
|
|
2168
|
+
status.textContent = 'Not saved: ' + e.message;
|
|
2169
|
+
status.style.color = 'var(--err)';
|
|
2170
|
+
}
|
|
2171
|
+
showToast('Failed to save: ' + e.message, true);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
|
|
1737
2175
|
async function loadStatus() {
|
|
1738
2176
|
try {
|
|
1739
2177
|
const res = await api('/api/status');
|
|
@@ -1941,6 +2379,7 @@
|
|
|
1941
2379
|
formLoadedKey = key;
|
|
1942
2380
|
formDirty = false;
|
|
1943
2381
|
currentProfileBuffer = JSON.parse(JSON.stringify(p));
|
|
2382
|
+
if (!currentProfileBuffer.modelEfforts) currentProfileBuffer.modelEfforts = {};
|
|
1944
2383
|
|
|
1945
2384
|
document.getElementById('form-title').textContent = 'Edit Profile: ' + p.name;
|
|
1946
2385
|
document.getElementById('p-key').value = key;
|
|
@@ -1953,6 +2392,11 @@
|
|
|
1953
2392
|
document.getElementById('p-outformat').value = p.outFormat || '';
|
|
1954
2393
|
document.getElementById('p-mode').value = p.mode || 'convert';
|
|
1955
2394
|
document.getElementById('p-thinking-mode').value = p.thinkingMode || 'auto';
|
|
2395
|
+
if (document.getElementById('p-thinking-effort')) {
|
|
2396
|
+
document.getElementById('p-thinking-effort').value = p.thinkingEffort || 'auto';
|
|
2397
|
+
}
|
|
2398
|
+
renderModelSlots(toolVal, p);
|
|
2399
|
+
syncEffortOptions();
|
|
1956
2400
|
document.getElementById('btn-delete').style.display = 'inline-flex';
|
|
1957
2401
|
|
|
1958
2402
|
const activeMap = state.activeProfiles || (state.config?.activeProfiles || {});
|
|
@@ -1961,8 +2405,6 @@
|
|
|
1961
2405
|
if (activeTag) {
|
|
1962
2406
|
activeTag.textContent = activeTargets.length ? '● Active: ' + activeTargets.join(', ') : '';
|
|
1963
2407
|
}
|
|
1964
|
-
|
|
1965
|
-
renderModelSlots(toolVal, p);
|
|
1966
2408
|
}
|
|
1967
2409
|
|
|
1968
2410
|
function initNewProfile() {
|
|
@@ -1977,7 +2419,8 @@
|
|
|
1977
2419
|
outFormat: 'openai-chat',
|
|
1978
2420
|
mode: 'convert',
|
|
1979
2421
|
thinkingMode: 'auto',
|
|
1980
|
-
defaultModels: {}
|
|
2422
|
+
defaultModels: {},
|
|
2423
|
+
modelEfforts: {}
|
|
1981
2424
|
};
|
|
1982
2425
|
|
|
1983
2426
|
document.getElementById('form-title').textContent = 'Add New Provider Profile';
|
|
@@ -1990,10 +2433,14 @@
|
|
|
1990
2433
|
document.getElementById('p-outformat').value = 'openai-chat';
|
|
1991
2434
|
document.getElementById('p-mode').value = 'convert';
|
|
1992
2435
|
document.getElementById('p-thinking-mode').value = 'auto';
|
|
2436
|
+
if (document.getElementById('p-thinking-effort')) {
|
|
2437
|
+
document.getElementById('p-thinking-effort').value = 'auto';
|
|
2438
|
+
}
|
|
2439
|
+
renderModelSlots('auto', currentProfileBuffer);
|
|
2440
|
+
syncEffortOptions();
|
|
1993
2441
|
document.getElementById('btn-delete').style.display = 'none';
|
|
1994
2442
|
document.getElementById('active-tag').textContent = '';
|
|
1995
2443
|
|
|
1996
|
-
renderModelSlots('auto', currentProfileBuffer);
|
|
1997
2444
|
switchTab('tab-general');
|
|
1998
2445
|
}
|
|
1999
2446
|
|
|
@@ -2068,6 +2515,8 @@
|
|
|
2068
2515
|
document.getElementById('p-outformat').value = t.outFormat;
|
|
2069
2516
|
document.getElementById('p-mode').value = t.mode;
|
|
2070
2517
|
document.getElementById('p-thinking-mode').value = t.thinkingMode;
|
|
2518
|
+
document.getElementById('p-thinking-effort').value = t.thinkingEffort || 'auto';
|
|
2519
|
+
syncEffortOptions();
|
|
2071
2520
|
currentProfileBuffer = { ...currentProfileBuffer, ...t };
|
|
2072
2521
|
formDirty = true;
|
|
2073
2522
|
renderModelSlots(t.tool || 'auto', t);
|
|
@@ -2123,6 +2572,13 @@
|
|
|
2123
2572
|
// already had the key.
|
|
2124
2573
|
const thinkingMode = document.getElementById('p-thinking-mode').value;
|
|
2125
2574
|
if (thinkingMode !== 'auto' || existing.thinkingMode) profilePayload.thinkingMode = thinkingMode;
|
|
2575
|
+
const thinkingEffort = document.getElementById('p-thinking-effort')?.value;
|
|
2576
|
+
if (thinkingEffort && (thinkingEffort !== 'auto' || existing.thinkingEffort)) {
|
|
2577
|
+
profilePayload.thinkingEffort = thinkingEffort;
|
|
2578
|
+
}
|
|
2579
|
+
if (currentProfileBuffer?.modelEfforts && Object.keys(currentProfileBuffer.modelEfforts).length > 0) {
|
|
2580
|
+
profilePayload.modelEfforts = { ...currentProfileBuffer.modelEfforts };
|
|
2581
|
+
}
|
|
2126
2582
|
|
|
2127
2583
|
if (keyVal === MASKED_KEY) {
|
|
2128
2584
|
profilePayload.apiKey = existing.apiKey || '';
|
|
@@ -2778,6 +3234,13 @@
|
|
|
2778
3234
|
// Setup log auto-refresh
|
|
2779
3235
|
toggleLogAutoRefresh();
|
|
2780
3236
|
|
|
3237
|
+
// Setup profile modal effort & bifrost change triggers
|
|
3238
|
+
document.getElementById('p-thinking-mode')?.addEventListener('change', syncEffortOptions);
|
|
3239
|
+
document.getElementById('p-mode')?.addEventListener('change', syncEffortOptions);
|
|
3240
|
+
document.getElementById('p-outformat')?.addEventListener('change', syncEffortOptions);
|
|
3241
|
+
document.getElementById('p-url')?.addEventListener('input', syncEffortOptions);
|
|
3242
|
+
document.getElementById('p-tool')?.addEventListener('change', syncEffortOptions);
|
|
3243
|
+
|
|
2781
3244
|
// Trigger initial route
|
|
2782
3245
|
handleRoute();
|
|
2783
3246
|
|