nexus-prime 7.9.28 → 7.9.29

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.
Files changed (36) hide show
  1. package/dist/dashboard/app/api.js +30 -11
  2. package/dist/dashboard/app/index.html +28 -3
  3. package/dist/dashboard/app/main.js +34 -20
  4. package/dist/dashboard/app/state.js +4 -0
  5. package/dist/dashboard/app/styles/board.css +75 -0
  6. package/dist/dashboard/app/styles/context-log.css +154 -3
  7. package/dist/dashboard/app/styles/learning.css +204 -0
  8. package/dist/dashboard/app/styles/memory.css +169 -1
  9. package/dist/dashboard/app/styles/tokens.css +5 -0
  10. package/dist/dashboard/app/views/board.js +91 -23
  11. package/dist/dashboard/app/views/context-log.js +130 -7
  12. package/dist/dashboard/app/views/learning.js +200 -0
  13. package/dist/dashboard/app/views/memory.js +139 -26
  14. package/dist/dashboard/app/views/repo.js +168 -5
  15. package/dist/dashboard/routes/events.js +71 -3
  16. package/dist/dashboard/routes/graph.js +50 -0
  17. package/dist/dashboard/routes/learning.d.ts +2 -0
  18. package/dist/dashboard/routes/learning.js +213 -0
  19. package/dist/dashboard/routes/run-artifacts.d.ts +5 -0
  20. package/dist/dashboard/routes/run-artifacts.js +107 -0
  21. package/dist/dashboard/routes/runtime.js +98 -38
  22. package/dist/dashboard/routes/surfaces.js +2 -1
  23. package/dist/dashboard/selectors/operate-selector.js +105 -12
  24. package/dist/dashboard/server.js +2 -0
  25. package/dist/dashboard/types.d.ts +4 -1
  26. package/dist/engines/index.d.ts +2 -0
  27. package/dist/engines/index.js +1 -0
  28. package/dist/engines/learning/index.d.ts +1 -0
  29. package/dist/engines/learning/index.js +1 -0
  30. package/dist/engines/learning-network.d.ts +150 -0
  31. package/dist/engines/learning-network.js +452 -0
  32. package/dist/engines/orchestrator/decision-spine.d.ts +30 -3
  33. package/dist/engines/orchestrator/decision-spine.js +195 -23
  34. package/dist/phantom/runtime.d.ts +14 -0
  35. package/dist/phantom/runtime.js +144 -4
  36. package/package.json +1 -1
@@ -8,6 +8,18 @@
8
8
  import { CACHE_TTL, S, bus } from './state.js';
9
9
 
10
10
  const _cache = new Map();
11
+ const DEFAULT_TIMEOUT_MS = 5_000;
12
+
13
+ function _timeoutFor(url, override) {
14
+ if (Number.isFinite(Number(override))) return Number(override);
15
+ if (url.includes('/api/tokens/')) return 1_800;
16
+ if (url.includes('/api/license')) return 2_500;
17
+ if (url.includes('/api/runtimes')) return 3_000;
18
+ if (url.includes('/api/dashboard/surface/')) return 4_000;
19
+ if (url.includes('/api/dashboard/summary')) return 4_500;
20
+ if (url.includes('/api/knowledge-topology')) return 7_000;
21
+ return DEFAULT_TIMEOUT_MS;
22
+ }
11
23
 
12
24
  function _scopedUrl(url) {
13
25
  if (typeof url !== 'string' || !url.startsWith('/api/')) return url;
@@ -27,32 +39,39 @@ function _scopedUrl(url) {
27
39
  * then kick off a background refresh. Callers get the fast synchronous hit
28
40
  * for p50 <5ms; the next render cycle gets the fresh value.
29
41
  */
30
- export async function api(url, ttl = CACHE_TTL) {
42
+ export async function api(url, ttl = CACHE_TTL, opts = {}) {
31
43
  const requestUrl = _scopedUrl(url);
32
44
  const hit = _cache.get(requestUrl);
33
45
  const fresh = !hit || (Date.now() - hit.ts >= ttl);
34
46
 
35
- // Return stale data synchronously while refresh fires in background.
36
- const refreshPromise = fresh ? _fetch(requestUrl) : Promise.resolve(hit.data);
37
-
38
- if (hit && !fresh) {
39
- // Background refresh — don't await, just schedule.
40
- _fetch(requestUrl).catch(() => {});
47
+ if (hit) {
48
+ if (fresh) {
49
+ _fetch(requestUrl, opts).catch(() => {});
50
+ bus.emit('api:stale', { url: requestUrl, ageMs: Date.now() - hit.ts });
51
+ } else {
52
+ _fetch(requestUrl, opts).catch(() => {});
53
+ }
54
+ return hit.data;
41
55
  }
42
56
 
43
- return refreshPromise;
57
+ return _fetch(requestUrl, opts);
44
58
  }
45
59
 
46
- async function _fetch(url) {
60
+ async function _fetch(url, opts = {}) {
61
+ const controller = new AbortController();
62
+ const timeout = setTimeout(() => controller.abort(), _timeoutFor(url, opts.timeoutMs));
47
63
  try {
48
- const r = await fetch(url);
64
+ const r = await fetch(url, { signal: controller.signal });
49
65
  if (!r.ok) return null;
50
66
  const d = await r.json();
51
67
  _cache.set(url, { data: d, ts: Date.now() });
52
68
  bus.emit('cache:updated', url);
53
69
  return d;
54
- } catch {
70
+ } catch (err) {
71
+ bus.emit('api:timeout', { url, error: err?.name === 'AbortError' ? 'timeout' : String(err?.message || err) });
55
72
  return null;
73
+ } finally {
74
+ clearTimeout(timeout);
56
75
  }
57
76
  }
58
77
 
@@ -12,6 +12,7 @@
12
12
  <link rel="stylesheet" href="./styles/workforce.css">
13
13
  <link rel="stylesheet" href="./styles/memory.css">
14
14
  <link rel="stylesheet" href="./styles/context-log.css">
15
+ <link rel="stylesheet" href="./styles/learning.css">
15
16
  <link rel="stylesheet" href="./styles/governance.css">
16
17
  <link rel="stylesheet" href="./styles/trust.css">
17
18
  <link rel="stylesheet" href="./styles/animation.css">
@@ -56,6 +57,7 @@ if(location.protocol==='file:'){
56
57
  <a class="nav-item" data-nav="runtime" href="#runtime">⚡ Runtime</a>
57
58
  <a class="nav-item" data-nav="workforce" href="#workforce">Workforce</a>
58
59
  <a class="nav-item" data-nav="memory" href="#memory">Memory</a>
60
+ <a class="nav-item" data-nav="learning" href="#learning">Learning</a>
59
61
  <a class="nav-item" data-nav="context-log" href="#context-log">Context Log</a>
60
62
  <a class="nav-item" data-nav="repo" href="#repo">Repo</a>
61
63
  <a class="nav-item" data-nav="knowledge" href="#knowledge">Knowledge</a>
@@ -111,6 +113,8 @@ if(location.protocol==='file:'){
111
113
  <div class="hero-stat"><div class="metric-val" id="m-ops">—</div><div class="metric-lbl">Operatives</div><canvas id="spark-ops" class="sparkline" width="80" height="24" aria-hidden="true"></canvas></div>
112
114
  </div>
113
115
 
116
+ <div id="model-router-panel" class="model-router-panel card" aria-label="Model and context router"></div>
117
+
114
118
  <!-- Live strip + kanban -->
115
119
  <div id="agents-live-strip"></div>
116
120
  <div id="kanban-board">
@@ -189,6 +193,7 @@ if(location.protocol==='file:'){
189
193
 
190
194
  <div class="memory-toolbar">
191
195
  <button class="btn btn-sm" id="mem-graph-max-btn">Maximize graph</button>
196
+ <button class="btn btn-sm" id="mem-graph-focus-btn">Focus selected</button>
192
197
  <button class="btn btn-sm" id="mem-browse-btn">Browse memories</button>
193
198
  </div>
194
199
  <div id="graph-container" class="card">
@@ -228,13 +233,33 @@ if(location.protocol==='file:'){
228
233
  <div id="context-log-view"></div>
229
234
  </section>
230
235
 
236
+ <!-- LEARNING ───────────────────────────────────────────────── -->
237
+ <section class="view-panel" data-view="learning" aria-label="Learning network">
238
+ <div id="learning-view"></div>
239
+ </section>
240
+
231
241
  <!-- REPO ───────────────────────────────────────────────────── -->
232
242
  <section class="view-panel" data-view="repo" aria-label="Repo graph">
233
243
  <div class="shd">Repo knowledge graph</div>
234
- <div id="repo-graph-header" style="display:flex;align-items:center;gap:12px;padding:0 2px 10px;font-size:var(--text-sm);color:var(--text-muted)">
244
+ <div id="repo-graph-header" class="repo-graph-header">
235
245
  <span id="repo-graph-meta">—</span>
236
- <button id="repo-build-btn" class="btn btn-sm" style="margin-left:auto">Build graph</button>
237
- <input id="repo-search-input" type="text" placeholder="Search nodes…" style="padding:3px 8px;border:1px solid var(--border);border-radius:4px;background:var(--surface-1);color:var(--text);font-size:var(--text-sm);width:180px" autocomplete="off">
246
+ <div class="repo-graph-actions">
247
+ <button id="repo-build-btn" class="btn btn-sm">Build graph</button>
248
+ <button id="repo-memory-focus-btn" class="btn btn-sm">Memory overlay</button>
249
+ <input id="repo-search-input" class="repo-graph-search" type="text" placeholder="Search nodes…" autocomplete="off">
250
+ <div class="repo-graph-controls" aria-label="Repo graph navigation controls">
251
+ <button id="repo-zoom-out-btn" class="repo-graph-control" title="Zoom out">-</button>
252
+ <button id="repo-zoom-in-btn" class="repo-graph-control" title="Zoom in">+</button>
253
+ <button id="repo-fit-btn" class="repo-graph-control repo-graph-control-wide" title="Fit graph">Fit</button>
254
+ <button id="repo-reset-btn" class="repo-graph-control repo-graph-control-wide" title="Reset view">Reset</button>
255
+ <button id="repo-pan-left-btn" class="repo-graph-control" title="Pan left">&lt;</button>
256
+ <button id="repo-pan-right-btn" class="repo-graph-control" title="Pan right">&gt;</button>
257
+ <button id="repo-pan-up-btn" class="repo-graph-control" title="Pan up">^</button>
258
+ <button id="repo-pan-down-btn" class="repo-graph-control" title="Pan down">v</button>
259
+ <button id="repo-graph-max-btn" class="repo-graph-control repo-graph-control-wide" title="Maximize graph">Max</button>
260
+ <span id="repo-viewport-badge" class="repo-viewport-badge">100%</span>
261
+ </div>
262
+ </div>
238
263
  </div>
239
264
  <div class="card" style="position:relative;overflow:hidden">
240
265
  <div id="repo-graph-container" style="width:100%;height:480px;position:relative">
@@ -15,6 +15,7 @@ import { init as initCmdBar } from './widgets/command-bar.js';
15
15
  import * as Board from './views/board.js';
16
16
  import * as Workforce from './views/workforce.js';
17
17
  import * as Memory from './views/memory.js';
18
+ import * as Learning from './views/learning.js';
18
19
  import * as ContextLog from './views/context-log.js';
19
20
  import * as Knowledge from './views/knowledge.js';
20
21
  import * as Repo from './views/repo.js';
@@ -54,6 +55,7 @@ navRegister('board', Board.load);
54
55
  navRegister('runtime', Runtime.load);
55
56
  navRegister('workforce', Workforce.load);
56
57
  navRegister('memory', Memory.load);
58
+ navRegister('learning', Learning.load);
57
59
  navRegister('context-log', ContextLog.load);
58
60
  navRegister('repo', Repo.load);
59
61
  navRegister('knowledge', Knowledge.load);
@@ -89,6 +91,10 @@ setOnEvent(evt => {
89
91
  if (tab === 'memory' && evt.category === 'memory') {
90
92
  _refreshMemorySoon();
91
93
  }
94
+ if (tab === 'learning' && (evt.category === 'memory' || String(evt.type||'').startsWith('orchestration.'))) {
95
+ bustCache('/api/learning/packets?limit=24');
96
+ Learning.load();
97
+ }
92
98
  if (tab === 'governance' && evt.category === 'darwin') {
93
99
  Governance.load();
94
100
  }
@@ -123,6 +129,9 @@ setOnEvent(evt => {
123
129
  if (tab === 'context-log') {
124
130
  ContextLog.load();
125
131
  }
132
+ if (tab === 'learning') {
133
+ Learning.load();
134
+ }
126
135
  }
127
136
  // Workspace root promoted (e.g. from bootstrap hint) — refresh header immediately.
128
137
  if (String(evt.type||'') === 'workspace.changed') {
@@ -184,8 +193,9 @@ async function loadProjects() {
184
193
  ]);
185
194
  S.projects = Array.isArray(projectsD) ? projectsD : (projectsD?.projects||[]);
186
195
  S.runtimes = Array.isArray(runtimesD) ? runtimesD : (runtimesD?.runtimes||[]);
187
- _selectDefaultRuntime();
196
+ const scopeChanged = _selectDefaultRuntime();
188
197
  _renderProjectSelectors();
198
+ return scopeChanged;
189
199
  }
190
200
 
191
201
  async function loadCompanies() {
@@ -210,7 +220,7 @@ function _renderProjectSelectors() {
210
220
  }
211
221
 
212
222
  function _selectDefaultRuntime() {
213
- if (S.scope.runtimeId || !(S.runtimes||[]).length) return;
223
+ if (S.scope.runtimeId || !(S.runtimes||[]).length) return false;
214
224
  const current = S.workspace?.repoRoot || S.workspace?.workspaceRoot || '';
215
225
  const candidates = (S.runtimes||[]).filter(r => r?.runtimeId);
216
226
  const exact = current ? candidates.find(r => r.repoIdentity?.repoRoot === current) : null;
@@ -228,7 +238,10 @@ function _selectDefaultRuntime() {
228
238
  return root && root !== '/';
229
239
  });
230
240
  const selected = (exact && !currentLooksLikeHome ? exact : null) || remoteRepo || deepestRepo || candidates[0];
231
- S.scope.runtimeId = selected?.runtimeId || null;
241
+ const nextRuntimeId = selected?.runtimeId || null;
242
+ const changed = nextRuntimeId !== S.scope.runtimeId;
243
+ S.scope.runtimeId = nextRuntimeId;
244
+ return changed;
232
245
  }
233
246
 
234
247
  /* ─────────────────── Header / ticker ─────────────────── */
@@ -344,29 +357,30 @@ async function bootstrap() {
344
357
  _renderHeader(false);
345
358
 
346
359
  await loadWorkspace();
347
- await loadProjects();
348
-
349
- // Fast first paint: board + companies + repo-tree + license
350
- const [, , , lic] = await Promise.all([
351
- Board.load(),
352
- loadCompanies(),
353
- loadRepoTree(),
354
- api('/api/license', 60000),
355
- ]);
356
- S.license = lic;
357
-
358
- // Populate agent selector
359
- const agentSel = $('ctx-agent');
360
- if (agentSel && S.synapseHealth.length) {
361
- agentSel.innerHTML = '<option value="">All agents</option>' +
362
- S.synapseHealth.map(o=>`<option value="${esc(o.id||o.operativeId)}">${esc(o.role||o.name||o.id||'agent')}</option>`).join('');
360
+ const scopeChanged = await loadProjects();
361
+ if (scopeChanged) {
362
+ bustCache('/api/workspace');
363
+ bustCache('/api/repo-tree');
364
+ await loadWorkspace();
363
365
  }
364
366
 
365
- // Wire router after first paint
367
+ // Wire router before slow side panels. Each view owns progressive loading.
366
368
  routerStart();
367
369
 
368
370
  // Connect SSE stream — SSE drives all live updates; no polling needed.
369
371
  connectSSE();
372
+
373
+ Promise.allSettled([
374
+ loadCompanies(),
375
+ loadRepoTree(),
376
+ api('/api/license', 60000, { timeoutMs: 2500 }).then(lic => { S.license = lic; }),
377
+ ]).then(() => {
378
+ const agentSel = $('ctx-agent');
379
+ if (agentSel && S.synapseHealth.length) {
380
+ agentSel.innerHTML = '<option value="">All agents</option>' +
381
+ S.synapseHealth.map(o=>`<option value="${esc(o.id||o.operativeId)}">${esc(o.role||o.name||o.id||'agent')}</option>`).join('');
382
+ }
383
+ });
370
384
  }
371
385
 
372
386
  // Start when DOM is ready
@@ -86,6 +86,10 @@ export const S = {
86
86
  contextLogRuns: [],
87
87
  contextLogSelectedRunId: null,
88
88
  contextLogSpine: null,
89
+ contextLogLearning: null,
90
+ learningSurface: null,
91
+ learningGraph: null,
92
+ learningSelectedPacketId: null,
89
93
 
90
94
  // Neural Stream HUD ring buffer (latest SSE events, restored from v3.8.0)
91
95
  neuralStream: [],
@@ -13,6 +13,81 @@
13
13
  .hero-stat { padding: 18px 22px; background: var(--bg-elevated); transition: background 0.15s; }
14
14
  .hero-stat:hover { background: var(--bg-panel); }
15
15
 
16
+ .model-router-panel {
17
+ margin: 0 0 16px;
18
+ padding: 14px;
19
+ background:
20
+ radial-gradient(circle at 18% 0%, rgba(0,212,255,0.12), transparent 30%),
21
+ radial-gradient(circle at 80% 10%, rgba(0,255,136,0.10), transparent 26%),
22
+ var(--bg-elevated);
23
+ overflow: hidden;
24
+ }
25
+ .model-router-head {
26
+ display: flex; justify-content: space-between; align-items: flex-start; gap: 12px;
27
+ margin-bottom: 10px;
28
+ }
29
+ .model-router-head span,
30
+ .model-lane-client {
31
+ display: block;
32
+ color: var(--text-dim);
33
+ font-family: var(--font-mono);
34
+ font-size: 0.68rem;
35
+ text-transform: uppercase;
36
+ letter-spacing: 0.06em;
37
+ }
38
+ .model-router-head strong {
39
+ display: block;
40
+ margin-top: 3px;
41
+ color: var(--text-main);
42
+ font: var(--title);
43
+ }
44
+ .model-router-lanes {
45
+ display: grid;
46
+ grid-template-columns: repeat(3, minmax(0, 1fr));
47
+ gap: 10px;
48
+ }
49
+ .model-lane {
50
+ min-height: 92px;
51
+ padding: 12px;
52
+ text-align: left;
53
+ color: var(--text-muted);
54
+ background: rgba(0,0,0,0.2);
55
+ border: 1px solid var(--border);
56
+ border-radius: var(--radius);
57
+ cursor: pointer;
58
+ transition: transform .24s var(--ease), border-color .24s var(--ease), background .24s var(--ease);
59
+ }
60
+ .model-lane:hover,
61
+ .model-lane.selected {
62
+ transform: translateY(-2px);
63
+ border-color: rgba(0,255,136,0.36);
64
+ background: rgba(0,255,136,0.06);
65
+ }
66
+ .model-lane strong {
67
+ display: block;
68
+ margin: 6px 0 2px;
69
+ color: var(--text-main);
70
+ font-family: var(--font-mono);
71
+ font-size: 1rem;
72
+ }
73
+ .model-lane small {
74
+ color: var(--text-dim);
75
+ font-family: var(--font-mono);
76
+ }
77
+ .model-router-reason {
78
+ margin-top: 10px;
79
+ color: var(--text-muted);
80
+ font-size: 0.78rem;
81
+ line-height: 1.45;
82
+ }
83
+
84
+ @media (max-width: 900px) {
85
+ #hero-stats,
86
+ .model-router-lanes {
87
+ grid-template-columns: 1fr;
88
+ }
89
+ }
90
+
16
91
  /* ── Kanban ── */
17
92
  #kanban-board {
18
93
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px;
@@ -7,6 +7,7 @@
7
7
 
8
8
  .context-log-rail,
9
9
  .context-log-summary,
10
+ .context-router-card,
10
11
  .context-log-row,
11
12
  .context-log-decision {
12
13
  background: var(--bg-elevated);
@@ -69,7 +70,7 @@
69
70
 
70
71
  .context-log-summary {
71
72
  display: grid;
72
- grid-template-columns: repeat(5, minmax(90px, 1fr));
73
+ grid-template-columns: repeat(7, minmax(82px, 1fr));
73
74
  gap: 10px;
74
75
  padding: 14px;
75
76
  margin-bottom: 14px;
@@ -99,13 +100,91 @@
99
100
  }
100
101
 
101
102
  .context-log-selection > div,
102
- .context-log-refs {
103
+ .context-log-refs,
104
+ .context-log-changed {
103
105
  display: flex;
104
106
  flex-wrap: wrap;
105
107
  gap: 4px;
106
108
  min-width: 0;
107
109
  }
108
110
 
111
+ .context-router-card {
112
+ padding: 13px 14px;
113
+ margin-bottom: 14px;
114
+ }
115
+
116
+ .context-router-card.context-router-advisory {
117
+ border-color: rgba(255,209,77,0.3);
118
+ background: linear-gradient(180deg, rgba(255,209,77,0.055), var(--bg-elevated));
119
+ }
120
+
121
+ .context-router-card.context-router-changed {
122
+ border-color: rgba(0,255,136,0.3);
123
+ background: linear-gradient(180deg, rgba(0,255,136,0.055), var(--bg-elevated));
124
+ }
125
+
126
+ .context-router-head {
127
+ display: flex;
128
+ justify-content: space-between;
129
+ align-items: flex-start;
130
+ gap: 14px;
131
+ margin-bottom: 8px;
132
+ }
133
+
134
+ .context-router-head span,
135
+ .context-router-grid span,
136
+ .context-log-changed > span {
137
+ display: block;
138
+ color: var(--text-dim);
139
+ font-family: var(--font-mono);
140
+ font-size: 0.68rem;
141
+ letter-spacing: 0.05em;
142
+ text-transform: uppercase;
143
+ }
144
+
145
+ .context-router-head strong {
146
+ display: block;
147
+ margin-top: 3px;
148
+ color: var(--text-main);
149
+ font-family: var(--font-mono);
150
+ font-size: 1rem;
151
+ }
152
+
153
+ .context-router-metrics {
154
+ display: flex;
155
+ flex-wrap: wrap;
156
+ justify-content: flex-end;
157
+ gap: 6px;
158
+ }
159
+
160
+ .context-router-metrics span {
161
+ padding: 4px 7px;
162
+ border: 1px solid var(--border);
163
+ border-radius: var(--radius);
164
+ background: rgba(0,0,0,0.18);
165
+ color: var(--text-muted);
166
+ }
167
+
168
+ .context-router-card p {
169
+ margin: 0 0 11px;
170
+ color: var(--text-muted);
171
+ font-size: 0.8rem;
172
+ line-height: 1.45;
173
+ }
174
+
175
+ .context-router-grid {
176
+ display: grid;
177
+ grid-template-columns: repeat(3, minmax(0, 1fr));
178
+ gap: 10px;
179
+ }
180
+
181
+ .context-router-grid > div {
182
+ display: flex;
183
+ flex-wrap: wrap;
184
+ gap: 5px;
185
+ min-width: 0;
186
+ }
187
+
109
188
  .context-log-grid {
110
189
  display: grid;
111
190
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
@@ -142,6 +221,15 @@
142
221
  line-height: 1.45;
143
222
  }
144
223
 
224
+ .context-log-changed {
225
+ margin-top: 7px;
226
+ }
227
+
228
+ .context-log-changed > span {
229
+ width: 100%;
230
+ margin-bottom: 2px;
231
+ }
232
+
145
233
  .context-log-decision strong {
146
234
  display: block;
147
235
  margin-bottom: 6px;
@@ -150,6 +238,68 @@
150
238
  line-height: 1.35;
151
239
  }
152
240
 
241
+ .context-link-chip {
242
+ cursor: pointer;
243
+ font: var(--caption-role);
244
+ }
245
+
246
+ .context-link-chip[data-context-target] {
247
+ border-color: rgba(0,212,255,0.28);
248
+ background: rgba(0,212,255,0.06);
249
+ }
250
+
251
+ .context-link-chip[data-context-target]:hover {
252
+ color: var(--text-main);
253
+ border-color: rgba(0,255,136,0.36);
254
+ background: rgba(0,255,136,0.08);
255
+ }
256
+
257
+ .context-learning-card {
258
+ padding: 12px;
259
+ margin-bottom: 14px;
260
+ border: 1px solid rgba(0,255,136,0.16);
261
+ border-radius: var(--radius);
262
+ background: linear-gradient(180deg, rgba(0,255,136,0.05), rgba(0,212,255,0.025));
263
+ }
264
+
265
+ .context-learning-list {
266
+ display: grid;
267
+ gap: 8px;
268
+ margin-top: 10px;
269
+ }
270
+
271
+ .context-learning-item {
272
+ padding: 9px 10px;
273
+ border: 1px solid var(--border);
274
+ border-radius: var(--radius);
275
+ background: rgba(0,0,0,0.18);
276
+ }
277
+
278
+ .context-learning-item span {
279
+ color: var(--secondary);
280
+ font-family: var(--font-mono);
281
+ font-size: 0.68rem;
282
+ }
283
+
284
+ .context-learning-item strong,
285
+ .context-learning-item em {
286
+ display: block;
287
+ margin-top: 4px;
288
+ }
289
+
290
+ .context-learning-item strong {
291
+ color: var(--text-main);
292
+ font-size: 0.8rem;
293
+ line-height: 1.35;
294
+ }
295
+
296
+ .context-learning-item em {
297
+ color: var(--text-muted);
298
+ font-size: 0.74rem;
299
+ font-style: normal;
300
+ line-height: 1.45;
301
+ }
302
+
153
303
  .context-log-empty {
154
304
  color: var(--text-dim);
155
305
  font-size: 0.72rem;
@@ -158,7 +308,8 @@
158
308
  @media (max-width: 980px) {
159
309
  .context-log-shell,
160
310
  .context-log-grid,
161
- .context-log-selection {
311
+ .context-log-selection,
312
+ .context-router-grid {
162
313
  grid-template-columns: 1fr;
163
314
  }
164
315
  .context-log-summary {