pi-weave 0.3.9 → 0.3.11

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.
@@ -67,8 +67,9 @@ export const THEME_CSS = `
67
67
  --weave-note-gutter:18px;
68
68
  /* The desk-and-page split. The workspace is a desk of instruments — tree,
69
69
  graph, rail, bars — on the --weave-bg ground; the note column is the one
70
- *page* lying on it: crust in both Catppuccin schemes, the ground that
71
- sits furthest from the desk (deep under dark, bright over light). Two
70
+ *page* lying on it: crust under dark, plain white under Latte (the
71
+ refresh's "quiet page": paper reads as paper), the ground that sits
72
+ furthest from the desk. Two
72
73
  voices in total: sans for instruments and prose alike, mono for data — a
73
74
  third face was tried (serif prose) and withdrawn at the user's call; two
74
75
  voices read calmer than three. The CSP allows nothing fetched, and both
@@ -79,7 +80,7 @@ export const THEME_CSS = `
79
80
  two overlays that float above the grid. Tag pills are 999px — a capsule
80
81
  shape, not a scale step. The gate test below refuses any literal px
81
82
  radius so the scale cannot drift back. */
82
- --weave-radius:4px;--weave-radius-pop:7px;
83
+ --weave-radius:6px;--weave-radius-pop:10px;
83
84
  /* The type ramp, role-named because sizes drift and roles stick. Nine
84
85
  steps; every font-size in this sheet is one of these vars — that is a
85
86
  gate, not a convention:
@@ -91,7 +92,7 @@ export const THEME_CSS = `
91
92
  body 13.5 the note column's prose
92
93
  subhead 14 palette input, note-body h2, ribbon icon buttons
93
94
  title 15 note-body h1
94
- display 20 the note's page title
95
+ display 26 the note's page title (20 before the refresh)
95
96
  Two earlier sizes were merged into neighbours: 9px glyphs to prov, and
96
97
  the 10px column overlines to caption (caps plus .09em tracking already
97
98
  read larger than their point size suggests). \`display\` and \`body\` are
@@ -106,7 +107,7 @@ export const THEME_CSS = `
106
107
  rubber stamp. */
107
108
  --weave-px-prov:9.5px;--weave-px-caption:10.5px;--weave-px-ui:11.5px;
108
109
  --weave-px-row:12px;--weave-px-base:13px;--weave-px-body:13.5px;
109
- --weave-px-subhead:14px;--weave-px-title:15px;--weave-px-display:20px;
110
+ --weave-px-subhead:14px;--weave-px-title:15px;--weave-px-display:26px;
110
111
  }
111
112
  @media (prefers-color-scheme: light){
112
113
  :root:not([data-weave-theme]){${paletteTokens("light")}}
@@ -163,10 +164,14 @@ body{font-size:var(--weave-px-base)}
163
164
  provenance glyph shape carries the distinction the colour swap drops;
164
165
  elsewhere the hues are unaffected. The palette's hit rows need the same
165
166
  remap: same selected ground, same failure. The label is restated last
166
- so the selected row is always readable. */
167
+ so the selected row is always readable. The ground itself is the Context
168
+ rail's selection: the --weave-new tint plus a 2px inset accent bar, kept
169
+ under the pointer too. It is the only tinted row: the tree is where you
170
+ are, not what the agent touched (the graph shows that). */
171
+ .weave-row-on,.weave-row-on:hover{background:var(--weave-new);box-shadow:inset 2px 0 0 var(--weave-accent)}
167
172
  .weave-row-on .weave-twisty,.weave-row-on .weave-kind,.weave-row-on .weave-prov,
168
173
  .weave-row-on .weave-meta,.weave-row-on .weave-label{color:var(--weave-fg)}
169
- .weave-hit-on .weave-hit-badge,.weave-hit-on .weave-hit-detail{color:var(--weave-fg)}
174
+ .weave-hit-on .weave-hit-icon,.weave-hit-on .weave-hit-detail,.weave-hit-on .weave-hit-meta{color:var(--weave-fg)}
170
175
  /* A newly-arrived node (a file or note added since the last update, ):
171
176
  one short highlight that fades while the label settles from bold back to
172
177
  normal. The class is computed from the frame diff in workspace.ts and
@@ -178,16 +183,6 @@ body{font-size:var(--weave-px-base)}
178
183
  40%{font-weight:700}
179
184
  100%{background:transparent;font-weight:400}
180
185
  }
181
- /* Recall trail (issue #78): a note the agent searched, read or wrote. The
182
- tint follows the graph's palette for the same kinds (warn → ok → accent)
183
- and steps down through three strength buckets as the glow fades. */
184
- .weave-row-trail{--weave-trail:var(--weave-warn);box-shadow:inset 2px 0 0 color-mix(in srgb,var(--weave-trail) var(--weave-trail-mix),transparent);background:color-mix(in srgb,var(--weave-trail) calc(var(--weave-trail-mix) / 6),transparent)}
185
- .weave-row-trail-read{--weave-trail:var(--weave-ok)}
186
- .weave-row-trail-wrote{--weave-trail:var(--weave-accent)}
187
- .weave-row-trail-3{--weave-trail-mix:90%}
188
- .weave-row-trail-2{--weave-trail-mix:55%}
189
- .weave-row-trail-1{--weave-trail-mix:25%}
190
- .weave-row-trail-wrote.weave-row-trail-3 .weave-label{font-weight:600}
191
186
  .weave-twisty{width:16px;height:16px;flex:none;display:inline-flex;align-items:center;justify-content:center;color:var(--weave-faint);cursor:pointer}
192
187
  .weave-kind{flex:none;color:var(--weave-faint);font-size:var(--weave-px-ui)}
193
188
  .weave-prov{flex:none;font-size:var(--weave-px-prov)}
@@ -245,7 +240,7 @@ body{font-size:var(--weave-px-base)}
245
240
  .weave-note-head{position:sticky;top:0;z-index:2;padding:14px var(--weave-note-gutter) 9px;background:var(--weave-page);border-bottom:1px solid var(--weave-line)}
246
241
  /* The page's largest voice, and the point of the review's headline finding:
247
242
  a 15px title sat 1.5px above the body it named. --weave-px-display puts the
248
- title where a page's title belongs, weight 650 for presence at 20px (600
243
+ title where a page's title belongs, weight 650 for presence at 26px (600
249
244
  reads thin at that size, 700 reads as a poster), tracking pulled in by a
250
245
  hair because large sans needs it. */
251
246
  .weave-note-title{margin:0 0 4px;font-size:var(--weave-px-display);font-weight:650;line-height:1.25;letter-spacing:-.005em;color:var(--weave-fg)}
@@ -262,20 +257,67 @@ body{font-size:var(--weave-px-base)}
262
257
  }
263
258
  .weave-note-open:hover{color:var(--weave-fg);background:var(--weave-line)}
264
259
  .weave-note-open-mark{display:inline-flex;line-height:0}
265
- /* Save / Done, chip-shaped: the same quiet row, and a control in a reading
266
- column should not outweigh the title. */
267
- .weave-note-edit,.weave-note-save{
260
+ /* The note's own actions, at the end of the meta line. History is a quiet
261
+ word with an icon; Edit is the one bordered control, and neither outweighs
262
+ the title. */
263
+ .weave-note-actions{margin-left:auto;display:inline-flex;align-items:center;gap:4px}
264
+ .weave-note-meta .weave-note-action{
265
+ display:inline-flex;align-items:center;gap:5px;
268
266
  font:inherit;font-size:var(--weave-px-caption);line-height:1;white-space:nowrap;cursor:pointer;
269
- color:var(--weave-dim);background:var(--weave-panel);padding:4px 7px;
270
- border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius);
271
- }
272
- .weave-note-edit:hover,.weave-note-save:hover{color:var(--weave-fg);border-color:var(--weave-accent)}
273
- /* Save is the row's only affirmative action, so the only one with the accent. */
274
- .weave-note-save{color:var(--weave-accent);border-color:var(--weave-accent)}
275
- .weave-note-save:disabled{color:var(--weave-faint);border-color:var(--weave-line-strong);cursor:default}
276
- /* History: a slim bar under the header. The step's added text is marked in
277
- the accent tint, the same strength the selected rows use. */
278
- .weave-note-history[aria-pressed="true"]{color:var(--weave-accent);border-color:var(--weave-accent)}
267
+ color:var(--weave-dim);background:none;padding:5px 8px;
268
+ border:1px solid transparent;border-radius:var(--weave-radius);
269
+ }
270
+ .weave-note-meta .weave-note-action .weave-icon{width:13px;height:13px}
271
+ .weave-note-meta .weave-note-action:hover{color:var(--weave-fg);background:var(--weave-new)}
272
+ .weave-note-meta .weave-note-action:disabled{color:var(--weave-faint);background:none;cursor:default}
273
+ .weave-note-meta .weave-note-edit{color:var(--weave-fg);background:var(--weave-page);border-color:var(--weave-line-strong)}
274
+ .weave-note-meta .weave-note-edit:hover{border-color:var(--weave-accent);background:var(--weave-page)}
275
+ .weave-note-meta .weave-note-history[aria-pressed="true"]{color:var(--weave-accent);background:var(--weave-new)}
276
+ /* Where the note lives, above its title: a crumb, not a heading. */
277
+ .weave-note-crumb{margin:0 0 6px;display:flex;align-items:center;gap:5px;font-size:var(--weave-px-caption);color:var(--weave-faint)}
278
+ .weave-note-crumb .weave-icon{width:12px;height:12px}
279
+ /* Who wrote it, in words, with the spine's colour as a dot. */
280
+ .weave-note-meta .weave-prov{display:inline-flex;align-items:center;gap:6px;font-size:var(--weave-px-ui);color:var(--weave-dim)}
281
+ .weave-prov-dot{width:7px;height:7px;border-radius:999px;background:var(--weave-spine,var(--weave-accent))}
282
+ /* The edit bar (E1): one row while editing, pinned over the text it edits.
283
+ Formatting on the left, where the words are; the save state and the two
284
+ ways out on the right. Saving is automatic, so Done is the only
285
+ affirmative control and the only one in the accent. */
286
+ .weave-editbar{
287
+ position:sticky;top:0;z-index:3;display:flex;align-items:center;gap:6px;flex-wrap:wrap;
288
+ min-height:40px;padding:5px 12px;font-size:var(--weave-px-ui);
289
+ background:var(--weave-page);border-bottom:1px solid var(--weave-line);
290
+ }
291
+ .weave-editbar .weave-icon{width:14px;height:14px}
292
+ .weave-editbar-badge{
293
+ display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:999px;
294
+ font-size:var(--weave-px-caption);font-weight:600;color:var(--weave-accent);background:var(--weave-new);
295
+ }
296
+ .weave-editbar-badge .weave-icon{width:12px;height:12px}
297
+ .weave-editbar-tools{display:inline-flex;align-items:center;gap:1px;margin-left:4px;padding-left:8px;border-left:1px solid var(--weave-line)}
298
+ .weave-editbar .weave-editbar-tool{
299
+ display:inline-grid;place-items:center;width:28px;height:28px;padding:0;cursor:pointer;
300
+ color:var(--weave-dim);background:none;border:0;border-radius:var(--weave-radius);
301
+ }
302
+ .weave-editbar .weave-editbar-tool:hover{color:var(--weave-fg);background:var(--weave-new)}
303
+ .weave-editbar-status{margin-left:auto;display:inline-flex;align-items:center;gap:5px;white-space:nowrap;font-size:var(--weave-px-caption);color:var(--weave-faint)}
304
+ .weave-editbar-pending,.weave-editbar-saving{color:var(--weave-dim)}
305
+ .weave-editbar-saved{color:var(--weave-ok)}
306
+ .weave-editbar-failed{color:var(--weave-bad);font-weight:600}
307
+ .weave-editbar .weave-editbar-retry{font:inherit;color:var(--weave-accent);background:none;border:0;padding:2px 4px;cursor:pointer;text-decoration:underline;text-underline-offset:2px}
308
+ .weave-editbar .weave-editbar-discard,.weave-editbar .weave-editbar-done{font:inherit;font-size:var(--weave-px-caption);line-height:1;white-space:nowrap;cursor:pointer;border-radius:var(--weave-radius)}
309
+ .weave-editbar .weave-editbar-discard{padding:7px 9px;color:var(--weave-dim);background:none;border:1px solid transparent}
310
+ .weave-editbar .weave-editbar-discard:hover{color:var(--weave-fg);background:var(--weave-new)}
311
+ .weave-editbar .weave-editbar-done{padding:7px 14px;font-weight:600;color:var(--weave-page);background:var(--weave-accent);border:1px solid var(--weave-accent)}
312
+ .weave-editbar .weave-editbar-done:hover{filter:brightness(1.08)}
313
+ .weave-editbar .weave-editbar-discard:disabled,.weave-editbar .weave-editbar-done:disabled{opacity:.5;cursor:default;filter:none}
314
+ /* A finalized note's opening TLDR, set apart as the first thing to read.
315
+ The label is the card's own; task boxes keep their row and lose the bullet. */
316
+ .weave-tldr{margin:0 0 18px;padding:12px 16px;background:var(--weave-new);border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius)}
317
+ .weave-tldr>p,.weave-tldr>ul,.weave-tldr>ol{margin:0 0 6px}
318
+ .weave-tldr>*:last-child{margin-bottom:0}
319
+ .weave-tldr ul:has(>li>input[type="checkbox"]){list-style:none;padding-left:2px}
320
+ .weave-tldr-label{display:block;margin-bottom:3px;font-size:var(--weave-px-prov);font-weight:700;letter-spacing:.08em;color:var(--weave-accent)}
279
321
  /* The replay band sits under the header like the header itself: full width,
280
322
  the note's gutter, one line of words, one line of controls. Dots on the
281
323
  track are the versions, coloured by who made them. */
@@ -306,25 +348,28 @@ body{font-size:var(--weave-px-base)}
306
348
  .weave-replay-controls button:disabled{color:var(--weave-faint);cursor:default;opacity:.6}
307
349
  .weave-replay-controls .weave-replay-play{color:var(--weave-page);background:var(--weave-accent);border-color:var(--weave-accent)}
308
350
  .weave-replay-controls .weave-replay-play:hover:not(:disabled){filter:brightness(1.08)}
309
- .weave-replay-track{--weave-replay-at:100%;position:relative;flex:1;min-width:80px;height:28px;margin-left:8px}
351
+ /* The change bars (the refresh's H2): one bar per version, as tall as the
352
+ change was big, coloured by who made it; the version on screen is ringed.
353
+ The day labels sit under the band. Heights and label positions arrive as
354
+ custom properties through Preact's CSSOM style path, never an attribute. */
355
+ .weave-replay-track{--weave-replay-at:100%;position:relative;flex:1;min-width:80px;height:46px;margin-left:8px}
310
356
  .weave-replay-track::before{
311
- content:"";position:absolute;left:7px;right:7px;top:13px;height:2px;border-radius:999px;
357
+ content:"";position:absolute;left:7px;right:7px;top:31px;height:2px;border-radius:999px;
312
358
  background:linear-gradient(to right,color-mix(in srgb,var(--weave-accent) 55%,transparent) var(--weave-replay-at),var(--weave-line-strong) var(--weave-replay-at));
313
359
  }
314
- .weave-replay-ticks{position:absolute;left:7px;right:7px;top:0;bottom:0;display:flex;justify-content:space-between;align-items:center;pointer-events:none}
315
- .weave-replay-tick{position:relative;width:0;height:0}
316
- .weave-replay-tick::before{
317
- content:"";position:absolute;left:-6px;top:-6px;width:12px;height:12px;box-sizing:border-box;border-radius:999px;
318
- background:var(--weave-who);border:2px solid var(--weave-panel);
319
- }
320
- .weave-replay-ahead::before{opacity:.4}
360
+ .weave-replay-bars{position:absolute;left:7px;right:7px;top:2px;height:26px;pointer-events:none}
361
+ .weave-replay-bar{position:absolute;bottom:0;left:var(--weave-bar-at,0);width:max(1px,min(8px,calc(100% / var(--weave-bars,1) - 2px)));transform:translateX(-50%);height:var(--weave-bar,14%);border-radius:var(--weave-radius);background:var(--weave-who);opacity:.85}
362
+ .weave-replay-ahead{opacity:.35}
363
+ .weave-replay-current{opacity:1;outline:2px solid var(--weave-accent);outline-offset:1px}
364
+ .weave-replay-days{position:absolute;left:7px;right:7px;top:34px;height:12px;pointer-events:none}
365
+ .weave-replay-day{position:absolute;left:var(--weave-day-at,0);transform:translateX(-2px);font-size:var(--weave-px-prov);line-height:12px;color:var(--weave-faint);white-space:nowrap}
321
366
  .weave-replay-slider{position:absolute;inset:0;width:100%;height:100%;margin:0;background:transparent;-webkit-appearance:none;appearance:none;cursor:pointer}
322
367
  .weave-replay-slider:disabled{cursor:default}
323
368
  .weave-replay-slider:focus{outline:none}
324
- .weave-replay-slider::-webkit-slider-runnable-track{height:28px;background:transparent}
325
- .weave-replay-slider::-moz-range-track{height:28px;background:transparent}
369
+ .weave-replay-slider::-webkit-slider-runnable-track{height:46px;background:transparent}
370
+ .weave-replay-slider::-moz-range-track{height:46px;background:transparent}
326
371
  .weave-replay-slider::-webkit-slider-thumb{
327
- -webkit-appearance:none;appearance:none;width:14px;height:14px;margin-top:7px;border-radius:999px;
372
+ -webkit-appearance:none;appearance:none;width:14px;height:14px;margin-top:25px;border-radius:999px;
328
373
  background:var(--weave-page);border:2px solid var(--weave-accent);box-sizing:border-box;
329
374
  }
330
375
  .weave-replay-slider::-moz-range-thumb{
@@ -342,7 +387,7 @@ body{font-size:var(--weave-px-base)}
342
387
  display:block;width:100%;flex:1 0 auto;min-height:12em;resize:none;
343
388
  padding:12px var(--weave-note-gutter) 28px;box-sizing:border-box;
344
389
  font-family:var(--weave-mono);font-size:var(--weave-px-row);line-height:1.6;
345
- color:var(--weave-fg);background:var(--weave-bg);border:0;outline:0;
390
+ color:var(--weave-fg);background:var(--weave-page);border:0;outline:0;
346
391
  }
347
392
  /* The \`## Raw\` box under the editor: folded by default so a long
348
393
  dictation never outgrows the note, a dashed edge to say "captured, not
@@ -516,6 +561,114 @@ body{font-size:var(--weave-px-base)}
516
561
  border-top:1px solid var(--weave-line);
517
562
  }
518
563
 
564
+ /* graph spotlight (G1) ---------------------------------------------------
565
+ The field floating top-left over the canvas, and the side sheet top-right.
566
+ Both are overlays on the stage, so they sit on the panel ground with a
567
+ hairline and the pop radius — no elevation shadow, per the sheet's rule.
568
+ The spotlight's voice is --weave-warn: the highlighter colour the WebGL
569
+ side paints lit edges in (graph.model.ts SPOTLIGHT_EDGE_SLOT), so the chip,
570
+ the legend, the marked words and the lit dots all say "lit" in one hue.
571
+ The sheet stops short of the control strip and count line under the
572
+ canvas (about 54px together) and scrolls inside itself. Button rules are
573
+ qualified by their overlay (.weave-sheet .weave-sheet-open) because the
574
+ workspace sheet's \`.weave-workbench button{font:inherit;color:inherit}\`
575
+ reset is installed later and would outrank a single class. */
576
+ .weave-spotlight{
577
+ position:absolute;z-index:3;top:10px;left:10px;width:min(330px,calc(100% - 20px));box-sizing:border-box;
578
+ display:flex;align-items:center;gap:6px;padding:3px 5px 3px 9px;
579
+ color:var(--weave-faint);background:var(--weave-panel);
580
+ border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius-pop);
581
+ }
582
+ /* The pill is the focus ring: the input inside it drops the global outline
583
+ and the frame takes a 2px accent edge (border plus an inset hairline). */
584
+ .weave-spotlight:focus-within{border-color:var(--weave-accent);box-shadow:inset 0 0 0 1px var(--weave-accent)}
585
+ .weave-spotlight-input:focus-visible{outline:none}
586
+ .weave-spotlight-on{color:var(--weave-warn)}
587
+ .weave-spotlight-icon{flex:none;display:block}
588
+ .weave-spotlight-input{
589
+ flex:1;min-width:0;height:24px;padding:0;font:inherit;font-size:var(--weave-px-row);
590
+ color:var(--weave-fg);background:transparent;border:0;outline:0;
591
+ }
592
+ .weave-spotlight-input::placeholder{color:var(--weave-faint)}
593
+ .weave-spotlight-input::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none}
594
+ .weave-spotlight-count{
595
+ flex:none;padding:1px 7px;font-size:var(--weave-px-caption);font-weight:650;white-space:nowrap;
596
+ color:var(--weave-warn);background:color-mix(in srgb,var(--weave-warn) 16%,transparent);border-radius:999px;
597
+ }
598
+ .weave-spotlight-none{color:var(--weave-dim);background:var(--weave-line)}
599
+ .weave-spotlight-by{flex:none;font-size:var(--weave-px-caption);color:var(--weave-dim);white-space:nowrap}
600
+ .weave-spotlight .weave-spotlight-clear{
601
+ flex:none;padding:1px 5px;cursor:pointer;font-family:var(--weave-mono);font-size:var(--weave-px-prov);
602
+ color:var(--weave-faint);background:transparent;border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius);
603
+ }
604
+ .weave-spotlight .weave-spotlight-clear:hover{color:var(--weave-fg);border-color:var(--weave-accent)}
605
+ .weave-legend-lit{color:var(--weave-warn)}
606
+ .weave-sheet{
607
+ position:absolute;z-index:3;top:10px;right:10px;width:min(300px,calc(100% - 20px));max-height:calc(100% - 74px);
608
+ box-sizing:border-box;overflow:auto;padding:12px 14px 14px;
609
+ font-size:var(--weave-px-base);color:var(--weave-fg);background:var(--weave-panel);
610
+ border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius-pop);
611
+ }
612
+ .weave-sheet .weave-sheet-close{
613
+ position:absolute;top:6px;right:6px;width:24px;height:24px;padding:0;cursor:pointer;
614
+ font-size:var(--weave-px-display);line-height:1;color:var(--weave-dim);
615
+ background:transparent;border:0;border-radius:var(--weave-radius);
616
+ }
617
+ .weave-sheet .weave-sheet-close:hover{color:var(--weave-fg);background:var(--weave-line)}
618
+ .weave-sheet-crumb{
619
+ margin:0 28px 2px 0;display:flex;align-items:center;gap:5px;min-width:0;
620
+ font-size:var(--weave-px-caption);color:var(--weave-faint);
621
+ }
622
+ .weave-sheet-crumb span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
623
+ .weave-sheet-title{
624
+ margin:0 20px 4px 0;font-size:var(--weave-px-title);font-weight:650;line-height:1.3;overflow-wrap:anywhere;
625
+ }
626
+ .weave-sheet-meta{margin:0;display:flex;align-items:center;gap:5px;font-size:var(--weave-px-ui);color:var(--weave-dim);text-transform:capitalize}
627
+ .weave-sheet-when{color:var(--weave-faint);text-transform:none}
628
+ .weave-sheet-text{
629
+ margin:10px 0 0;padding:8px 10px;line-height:1.55;overflow-wrap:anywhere;
630
+ background:var(--weave-new);border:1px solid color-mix(in srgb,var(--weave-accent) 30%,transparent);
631
+ border-radius:var(--weave-radius-pop);
632
+ }
633
+ .weave-sheet-text-head{
634
+ display:block;margin:0 0 2px;font-size:var(--weave-px-prov);font-weight:700;letter-spacing:.09em;
635
+ text-transform:uppercase;color:var(--weave-accent);
636
+ }
637
+ .weave-sheet-mark{border-radius:var(--weave-radius)}
638
+ .weave-sheet-why{margin:8px 0 0;display:flex;gap:5px;font-size:var(--weave-px-caption);color:var(--weave-dim)}
639
+ .weave-sheet-why span{color:var(--weave-warn)}
640
+ .weave-sheet-section{margin:12px 0 0}
641
+ .weave-sheet-heading{
642
+ margin:0 0 3px;display:flex;align-items:center;font-size:var(--weave-px-caption);font-weight:600;
643
+ letter-spacing:.09em;text-transform:uppercase;color:var(--weave-faint);
644
+ }
645
+ .weave-sheet-count{margin-left:auto;font-family:var(--weave-mono);letter-spacing:0}
646
+ .weave-sheet-rows{margin:0;padding:0;list-style:none}
647
+ .weave-sheet .weave-sheet-row{
648
+ display:flex;align-items:center;gap:7px;width:100%;padding:3px 0;cursor:pointer;text-align:left;
649
+ font:inherit;font-size:var(--weave-px-row);color:var(--weave-fg);background:none;border:0;
650
+ }
651
+ .weave-sheet .weave-sheet-row:hover{color:var(--weave-accent)}
652
+ .weave-sheet-row-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
653
+ .weave-sheet-dot{flex:none;width:6px;height:6px;border-radius:999px;background:var(--weave-faint)}
654
+ .weave-sheet-dot-lit{background:var(--weave-warn)}
655
+ .weave-sheet-more{margin:2px 0 0;font-size:var(--weave-px-caption);color:var(--weave-faint)}
656
+ .weave-sheet-actions{margin:14px 0 0;display:flex;gap:6px;flex-wrap:wrap}
657
+ .weave-sheet .weave-sheet-open,.weave-sheet .weave-sheet-split{
658
+ padding:5px 11px;cursor:pointer;font:inherit;font-size:var(--weave-px-ui);font-weight:600;
659
+ border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius);
660
+ }
661
+ .weave-sheet .weave-sheet-open{color:var(--weave-page);background:var(--weave-accent);border-color:var(--weave-accent)}
662
+ .weave-sheet .weave-sheet-open:hover{filter:brightness(1.08)}
663
+ .weave-sheet .weave-sheet-split{color:var(--weave-fg);background:var(--weave-page)}
664
+ .weave-sheet .weave-sheet-split:hover{border-color:var(--weave-accent)}
665
+ /* Side by side the field (330) and the sheet (300) need about 660px of
666
+ stage; a narrower graph — a split pane, a small window — drops the sheet
667
+ under the field instead of letting the two overlap. */
668
+ .weave-graph{container-type:inline-size}
669
+ @container (max-width:680px){.weave-sheet{top:50px;max-height:calc(100% - 114px)}}
670
+ /* end graph spotlight ---------------------------------------------------- */
671
+
519
672
  /* context rail ---------------------------------------------------------- */
520
673
  /* The \`.weave-empty\` block that used to sit above went with \`EmptyState.tsx\`
521
674
  in P3: every column now has its own empty state (\`treeEmptyMessage\`,
@@ -535,9 +688,8 @@ body{font-size:var(--weave-px-base)}
535
688
  .weave-trail{padding-top:6px;padding-bottom:6px;border-bottom:1px solid var(--weave-line)}
536
689
  .weave-trail-head{display:flex;align-items:center;gap:6px;font-size:var(--weave-px-caption);font-weight:600;letter-spacing:.09em;color:var(--weave-faint);text-transform:uppercase}
537
690
  .weave-trail-head .weave-ctx-count{margin-left:0}
538
- .weave-trail-replay,.weave-trail-turn{font:inherit;font-size:var(--weave-px-caption);letter-spacing:0;text-transform:none;color:var(--weave-dim);background:none;border:1px solid var(--weave-line);border-radius:var(--weave-radius);padding:0 6px;cursor:pointer}
539
- .weave-trail-replay{margin-left:auto}
540
- .weave-trail-replay:hover,.weave-trail-turn:hover{color:var(--weave-fg)}
691
+ .weave-trail-turn{font:inherit;font-size:var(--weave-px-caption);letter-spacing:0;text-transform:none;color:var(--weave-dim);background:none;border:1px solid var(--weave-line);border-radius:var(--weave-radius);padding:0 6px;cursor:pointer}
692
+ .weave-trail-turn:hover{color:var(--weave-fg)}
541
693
  .weave-trail-turn-on{color:var(--weave-accent);border-color:var(--weave-accent)}
542
694
  .weave-trail-steps{margin:4px 0 0;padding:0;list-style:none;display:flex;flex-direction:column;gap:2px}
543
695
  .weave-trail-step{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px;padding-left:10px;position:relative;line-height:1.45}
@@ -596,6 +748,7 @@ body{font-size:var(--weave-px-base)}
596
748
  .weave-status-cwd,.weave-status-sel{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
597
749
  .weave-status-cwd{max-width:38%;color:var(--weave-faint)}
598
750
  .weave-status-sel{color:var(--weave-fg)}
751
+ .weave-status-hint{margin-left:auto;padding-left:8px;white-space:nowrap;color:var(--weave-faint)}
599
752
 
600
753
  /* overlays: the ⌘K palette and the ? help sheet (P4) --------------------- */
601
754
  /* The scrim is a click target that closes, and the reason both overlays sit
@@ -611,31 +764,77 @@ body{font-size:var(--weave-px-base)}
611
764
  overflow:hidden;background:var(--weave-panel);color:var(--weave-fg);
612
765
  border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius-pop);
613
766
  }
767
+ /* The ⌘K palette, grouped and compact: an input row (glyph, query, Esc), a
768
+ scope row of pills that Tab cycles, one line per result under small
769
+ capitals group heads, and a footer of key hints. Wider than the help sheet
770
+ because a row carries a title, a snippet and a meta column side by side. */
771
+ .weave-palette{max-width:640px}
772
+ .weave-palette-bar{
773
+ flex:none;display:flex;align-items:center;gap:8px;padding:0 11px;
774
+ border-bottom:1px solid var(--weave-line);
775
+ }
776
+ .weave-palette-glyph{color:var(--weave-faint)}
614
777
  .weave-palette-input{
615
- flex:none;height:34px;padding:0 11px;font:inherit;font-size:var(--weave-px-subhead);
616
- color:var(--weave-fg);background:transparent;border:0;
617
- border-bottom:1px solid var(--weave-line);border-radius:0;
778
+ flex:1;min-width:0;height:36px;padding:0;font:inherit;font-size:var(--weave-px-subhead);
779
+ color:var(--weave-fg);background:transparent;border:0;border-radius:0;
780
+ }
781
+ .weave-palette-input:focus-visible{outline:0}
782
+ .weave-palette-input::-webkit-search-cancel-button{display:none}
783
+ .weave-palette-bar:focus-within{box-shadow:inset 0 -1px 0 var(--weave-accent)}
784
+ .weave-palette-kbd{
785
+ flex:none;padding:0 4px;font-family:inherit;font-size:var(--weave-px-prov);line-height:14px;
786
+ color:var(--weave-faint);border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius);
787
+ }
788
+ .weave-palette-scopes{
789
+ flex:none;display:flex;flex-wrap:wrap;gap:3px;padding:5px 8px;
790
+ border-bottom:1px solid var(--weave-line);
618
791
  }
619
- .weave-palette-input:focus-visible{outline-offset:-2px}
620
- .weave-hits{flex:1;min-height:0;overflow:auto;margin:0;padding:3px 0;list-style:none}
792
+ .weave-palette-scope{
793
+ font:inherit;font-size:var(--weave-px-caption);line-height:1;padding:3px 9px;cursor:pointer;
794
+ color:var(--weave-dim);background:none;border:0;border-radius:999px;
795
+ }
796
+ .weave-palette-scope:hover{color:var(--weave-fg);background:var(--weave-line)}
797
+ .weave-palette-scope[aria-pressed="true"]{color:var(--weave-panel);background:var(--weave-fg);font-weight:600}
798
+ .weave-palette-scope-count{font-variant-numeric:tabular-nums}
799
+ .weave-hits{flex:1;min-height:0;overflow:auto;margin:0;padding:0 0 4px;list-style:none}
800
+ .weave-hit-section{list-style:none}
801
+ .weave-hit-group{
802
+ padding:7px 11px 2px;font-size:var(--weave-px-prov);font-weight:700;letter-spacing:.08em;
803
+ text-transform:uppercase;color:var(--weave-faint);
804
+ }
805
+ .weave-hit-rows{margin:0;padding:0;list-style:none}
621
806
  .weave-hit{
622
- display:flex;align-items:baseline;gap:7px;padding:3px 11px;cursor:pointer;
623
- min-width:0;white-space:nowrap;
807
+ display:flex;align-items:center;gap:8px;padding:3px 11px;cursor:pointer;
808
+ min-width:0;white-space:nowrap;font-size:var(--weave-px-row);
624
809
  }
625
810
  .weave-hit-on{background:var(--weave-new);box-shadow:inset 2px 0 0 var(--weave-accent)}
626
- .weave-hit-badge{
627
- flex:none;font-size:var(--weave-px-prov);letter-spacing:.06em;text-transform:uppercase;color:var(--weave-faint);
628
- min-width:56px;
629
- }
630
- .weave-hit-note .weave-hit-badge{color:var(--weave-accent)}
631
- .weave-hit-label{flex:none;max-width:46%;overflow:hidden;text-overflow:ellipsis}
811
+ .weave-hit-icon{color:var(--weave-faint)}
812
+ .weave-hit-notes .weave-hit-icon{color:var(--weave-accent)}
813
+ .weave-hit-label{flex:none;max-width:46%;overflow:hidden;text-overflow:ellipsis;font-weight:600;color:var(--weave-fg)}
814
+ .weave-hit-mono{font-family:var(--weave-mono);font-size:var(--weave-px-ui);font-weight:500}
632
815
  .weave-hit-detail{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;font-size:var(--weave-px-ui);color:var(--weave-dim)}
816
+ /* The match marks (search rows, the graph sheet) are a find-in-page amber.
817
+ Dark themes tint their own light --weave-warn, and the marked words go
818
+ white: the theme's text on that tint measured 3.3–3.6:1 in the Catppuccin
819
+ darks, white is 5.5:1 or better in every dark theme. A light theme's
820
+ --weave-warn is a deep brown that tints to mud, so light schemes take a
821
+ fixed amber under their own text instead. */
822
+ .weave-hit-mark,.weave-sheet-mark{color:#fff;background:color-mix(in srgb,var(--weave-warn) 28%,transparent)}
823
+ .weave-hit-mark{border-radius:var(--weave-radius);padding:0 1px}
824
+ :root[data-weave-scheme="light"] .weave-hit-mark,:root[data-weave-scheme="light"] .weave-sheet-mark{color:var(--weave-fg);background:color-mix(in srgb,#f2a93b 34%,transparent)}
825
+ .weave-hit-meta{flex:none;margin-left:auto;font-size:var(--weave-px-caption);color:var(--weave-faint);font-variant-numeric:tabular-nums}
633
826
  .weave-palette-status{margin:0;padding:14px 11px;color:var(--weave-dim);line-height:1.5}
634
827
  .weave-palette-foot,.weave-help-foot{
635
828
  display:flex;gap:12px;margin:0;padding:4px 11px;font-size:var(--weave-px-caption);color:var(--weave-faint);
636
829
  border-top:1px solid var(--weave-line);
637
830
  }
638
- .weave-palette-hint,.weave-help-hint{margin-left:auto}
831
+ .weave-palette-foot{align-items:center;flex-wrap:wrap;row-gap:2px;min-height:26px;box-sizing:border-box;background:var(--weave-bg)}
832
+ .weave-palette-light{
833
+ margin-left:auto;display:flex;align-items:center;gap:6px;padding:0;cursor:pointer;white-space:nowrap;
834
+ font:inherit;font-weight:600;color:var(--weave-accent);background:none;border:0;
835
+ }
836
+ .weave-palette-light .weave-palette-kbd{color:inherit;border-color:currentColor}
837
+ .weave-help-hint{margin-left:auto}
639
838
  .weave-help-title{margin:0;padding:9px 11px;font-size:var(--weave-px-base);font-weight:600;border-bottom:1px solid var(--weave-line)}
640
839
  .weave-keys{flex:1;min-height:0;overflow:auto;margin:0;padding:5px 11px}
641
840
  .weave-key-group{margin:0 0 3px;font-size:var(--weave-px-prov);letter-spacing:.09em;text-transform:uppercase;color:var(--weave-faint)}
@@ -5,7 +5,7 @@ export const WORKSPACE_CSS = `
5
5
  .weave-workbench button:focus-visible,.weave-workbench summary:focus-visible{outline:2px solid var(--weave-accent);outline-offset:-2px}
6
6
  .weave-workbench button:disabled{opacity:.35;cursor:default}
7
7
  .weave-ribbon{width:44px;flex:none;display:flex;align-items:center;flex-direction:column;gap:8px;padding:12px 0;background:var(--weave-panel);border-right:1px solid var(--weave-line)}
8
- .weave-ribbon button{width:32px;height:32px;border:0;border-radius:var(--weave-radius);background:transparent;font-size:var(--weave-px-display);color:var(--weave-dim)}
8
+ .weave-ribbon button{width:32px;height:32px;display:grid;place-items:center;border:0;border-radius:var(--weave-radius);background:transparent;font-size:var(--weave-px-display);color:var(--weave-dim)}
9
9
  .weave-ribbon button:hover,.weave-ribbon button[aria-pressed=true]{color:var(--weave-accent);background:var(--weave-new)}
10
10
  .weave-ribbon-space{flex:1}
11
11
  .weave-sidebar{flex:none;min-width:0;display:flex;flex-direction:column;background:var(--weave-panel);overflow:hidden}
@@ -65,20 +65,24 @@ export const WORKSPACE_CSS = `
65
65
  .weave-pane-menu button{text-align:left;border:0;background:none;padding:9px 10px;font-size:var(--weave-px-row)}
66
66
  .weave-pane-menu button:hover{background:var(--weave-new)}
67
67
  .weave-pane-content{min-width:0;min-height:0;display:flex;overflow:hidden;position:relative}
68
- .weave-document{display:flex;min-width:0;min-height:0;flex:1}
68
+ /* The reading column's gutters. Centred at the reading width; where the
69
+ floating Context card covers the right margin, the right gutter grows to
70
+ clear it and the column slides left before it narrows. */
71
+ .weave-document{display:flex;min-width:0;min-height:0;flex:1;
72
+ --weave-gutter-c:max(24px,calc((100% - var(--weave-reading-width,760px))/2));
73
+ --weave-gutter-r:max(var(--weave-gutter-c),var(--weave-card-space,0px));
74
+ --weave-gutter-l:max(24px,min(var(--weave-gutter-c),calc(100% - var(--weave-reading-width,760px) - var(--weave-gutter-r))))}
69
75
  .weave-document .weave-note{width:100%;border-left:0}
70
- .weave-document .weave-note-head{position:static;padding:28px max(24px,calc((100% - var(--weave-reading-width,760px))/2)) 18px;border:0}
71
- .weave-document .weave-note-title{font-size:var(--weave-px-display);letter-spacing:-.03em;margin-bottom:12px}
72
- .weave-document .weave-note-body{padding:10px max(24px,calc((100% - var(--weave-reading-width,760px))/2)) 64px;font-size:var(--weave-note-size,var(--weave-px-subhead));line-height:1.8}
76
+ .weave-document .weave-note-head{position:static;padding:28px var(--weave-gutter-r) 18px var(--weave-gutter-l);border:0}
77
+ .weave-document .weave-note-title{font-size:var(--weave-px-display);letter-spacing:-.03em;margin-bottom:10px}
78
+ .weave-document .weave-note-body{padding:10px var(--weave-gutter-r) 64px var(--weave-gutter-l);font-size:var(--weave-note-size,var(--weave-px-subhead));line-height:1.8}
73
79
  .weave-document .weave-note-body h1{font-size:var(--weave-px-display);line-height:1.4;margin-bottom:18px}
74
80
  .weave-document .weave-note-body h2{font-size:var(--weave-px-title);margin-top:28px}
75
- .weave-document .weave-note-editor{padding:14px max(24px,calc((100% - var(--weave-reading-width,760px))/2));min-height:220px;flex:1;font-size:var(--weave-note-size,var(--weave-px-subhead))}
81
+ .weave-document .weave-note-editor{padding:14px var(--weave-gutter-r) 14px var(--weave-gutter-l);min-height:220px;flex:1;font-size:var(--weave-note-size,var(--weave-px-subhead))}
76
82
  .weave-document .weave-note-tags{margin-top:12px}
77
- .weave-document .weave-replay{padding:12px max(24px,calc((100% - var(--weave-reading-width,760px))/2)) 14px}
78
- .weave-document .weave-raw{margin:0 max(24px,calc((100% - var(--weave-reading-width,760px))/2)) 28px}
79
- .weave-document .weave-note-edit,.weave-document .weave-note-save{padding:5px 10px;border-radius:var(--weave-radius);font-size:var(--weave-px-caption)}
80
- .weave-document .weave-note-edit{color:var(--weave-dim);background:transparent;border-color:transparent}
81
- .weave-document .weave-note-edit:hover{color:var(--weave-accent);background:var(--weave-new);border-color:transparent}
83
+ .weave-document .weave-replay{padding:12px var(--weave-gutter-r) 14px var(--weave-gutter-l)}
84
+ .weave-document .weave-raw{margin:0 var(--weave-gutter-r) 28px var(--weave-gutter-l)}
85
+ .weave-document .weave-editbar{padding-right:calc(12px + var(--weave-card-space,0px))}
82
86
  .weave-welcome{align-self:center;margin:auto;padding:35px;max-width:560px;color:var(--weave-dim);line-height:1.7}
83
87
  .weave-welcome-mark{font-size:var(--weave-px-display);color:var(--weave-accent)}
84
88
  .weave-welcome h1{font-size:var(--weave-px-display);line-height:1.4;letter-spacing:-.025em;color:var(--weave-fg);font-weight:550}
@@ -117,6 +121,29 @@ export const WORKSPACE_CSS = `
117
121
  .weave-footer>.weave-status{flex:1;min-width:0;border:0}
118
122
  .weave-footer>span{padding:4px 10px;white-space:nowrap}
119
123
  .weave-footer>button{font:inherit;color:var(--weave-fg);background:none;border:0;padding:7px 12px;cursor:pointer}
124
+ /* Context as a floating card (C1): over the page's right margin, below the
125
+ pane toolbar, folding to a pill in the toolbar strip. Pinned, it is the
126
+ column it always was. The shadow is the card's only elevation, and lives
127
+ here rather than in THEME_CSS, whose surfaces are flat by rule. */
128
+ .weave-context-dock.weave-context-float{position:absolute;top:90px;right:14px;bottom:auto;z-index:10;width:var(--weave-card-width);max-height:calc(100% - 104px);border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius-pop);background:var(--weave-panel);box-shadow:0 12px 32px #0000001f,0 2px 6px #00000014;transition:opacity 160ms ease,visibility 0s}
129
+ .weave-context-dock.weave-context-float[data-open=false]{width:var(--weave-card-width);opacity:0;visibility:hidden;transition:opacity 160ms ease,visibility 0s 160ms}
130
+ .weave-context-float>.weave-workspace-divider{display:none}
131
+ .weave-context-float>.weave-sidebar,.weave-context-float[data-open=false]>.weave-sidebar{width:100%;max-height:100%;border:0;background:none;transform:none}
132
+ .weave-context-float .weave-sidebar-heading{height:38px;padding:0 6px 0 12px;border-bottom:1px solid var(--weave-line)}
133
+ .weave-context-float .weave-rail{padding:4px 12px 10px}
134
+ .weave-context-float .weave-context-graph-slot{max-height:calc(100dvh - 260px)}
135
+ .weave-context-float .weave-context-tags{height:auto!important;max-height:220px}
136
+ .weave-context-float .weave-ctx-group>.weave-workspace-divider{display:none}
137
+ .weave-context-heading strong{color:var(--weave-fg)}
138
+ .weave-context-count{min-width:18px;padding:1px 6px;border-radius:999px;background:var(--weave-raise);color:var(--weave-dim);font-size:var(--weave-px-caption);line-height:1.4;text-align:center}
139
+ .weave-sidebar-heading .weave-context-pin{margin-left:auto}
140
+ .weave-context-heading button:last-child{margin-left:0}
141
+ .weave-context-heading .weave-context-pin[aria-pressed=true]{color:var(--weave-accent)}
142
+ .weave-context-heading button{display:grid;place-items:center;width:28px;height:28px;padding:0}
143
+ .weave-context-pill{position:absolute;top:47px;right:54px;z-index:10;display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 6px 0 9px;border:1px solid var(--weave-line-strong);border-radius:999px;background:var(--weave-panel);color:var(--weave-dim)!important;font-size:var(--weave-px-caption)!important;box-shadow:0 2px 8px #00000014}
144
+ .weave-context-pill:hover{color:var(--weave-fg)!important;border-color:var(--weave-accent)}
145
+ .weave-context-pill .weave-icon{width:13px;height:13px}
146
+ .weave-pane-under-card{--weave-card-space:calc(var(--weave-card-width) + 28px)}
120
147
  @media(max-width:1049px){
121
148
  .weave-context-dock{position:absolute;right:0;top:0;bottom:0;z-index:10;width:var(--weave-sidebar-width)}
122
149
  .weave-sidebar-context{border-left:1px solid var(--weave-line-strong)}
@@ -27,7 +27,6 @@ export function TrailStrip(props: TrailStripProps) {
27
27
  <h4 class="weave-ctx-head weave-trail-head">
28
28
  <span class="weave-trail-title">{turnHeading(shown, shown === latest)}</span>
29
29
  <span class="weave-ctx-count" title="Notes read or written">{shown.used}</span>
30
- {shown.steps.length === 0 ? null : <button type="button" class="weave-trail-replay" title="Replay this turn's glow" onClick={() => props.onReplay(shown.turn)}>Replay</button>}
31
30
  </h4>
32
31
  {shown.steps.length === 0 ? <p class="weave-trail-empty">{TRAIL_EMPTY_TURN}</p> : null}
33
32
  <ol class="weave-trail-steps">
@@ -9,6 +9,8 @@
9
9
  * steps one after another from the moment it was clicked.
10
10
  * - {@link trailTurns}: the "This turn" strip — each turn's steps in order,
11
11
  * worded as `searched "auth" → read Auth migration → appended Auth migration`.
12
+ * - {@link agentSearch}: the agent's newest search this turn, which the
13
+ * graph view lights up in its spotlight ({@link agentSpotlight}).
12
14
  *
13
15
  * Pure, DOM-free and clock-free (every function takes `now`), like the other
14
16
  * `*.model.ts` files, so the whole feature is covered by unit tests.
@@ -179,17 +181,70 @@ export function trailTurns(trail: TrailPayload | null, graph: GraphPayload | nul
179
181
  return views.slice(0, TRAIL_TURNS_SHOWN);
180
182
  }
181
183
 
182
- /** The strip's heading for a turn: the latest is "This turn", older ones are their time. */
183
184
  /** What the strip says for a turn with no steps yet. */
184
185
  export const TRAIL_EMPTY_TURN = "Nothing recalled yet.";
185
186
 
187
+ /** The strip's heading for a turn: the latest is "This turn", older ones are their time. */
186
188
  export function turnHeading(view: TrailTurnView, latest: boolean): string {
187
189
  return latest ? "This turn" : view.time === "" ? `Turn ${view.turn}` : view.time;
188
190
  }
189
191
 
190
- /** The tree row's class for a glow, or `""`. Strength is bucketed so the class list stays small. */
191
- export function trailRowClass(mark: TrailMark | undefined): string {
192
- if (mark === undefined) return "";
193
- const level = mark.strength > 0.66 ? 3 : mark.strength > 0.33 ? 2 : 1;
194
- return ` weave-row-trail weave-row-trail-${mark.kind} weave-row-trail-${level}`;
192
+ // --- the agent's search in the graph view's spotlight ----------------------------------
193
+
194
+ /** The agent's newest search this turn: what the graph view's spotlight lights. */
195
+ export interface AgentSearch {
196
+ /** The event's `seq`, so a new search is a new number even for the same words. */
197
+ seq: number;
198
+ query: string;
199
+ /** The notes the search returned, as node ids. */
200
+ ids: readonly string[];
201
+ }
202
+
203
+ /**
204
+ * The agent's newest search in the current turn, or `null`.
205
+ *
206
+ * Only the current turn counts, so the spotlight a search lit goes out when
207
+ * the agent moves on to the next request. A search with no words (the wire
208
+ * allows one) has nothing to spell in the field and is skipped.
209
+ */
210
+ export function agentSearch(trail: TrailPayload | null): AgentSearch | null {
211
+ if (trail === null) return null;
212
+ for (let index = trail.events.length - 1; index >= 0; index--) {
213
+ const event = trail.events[index]!;
214
+ if (event.turn !== trail.turn) return null;
215
+ if (event.kind !== "searched" || event.query === undefined || event.query.trim() === "") continue;
216
+ return { seq: event.seq, query: event.query, ids: event.slugs.map((slug) => `note:${slug}`) };
217
+ }
218
+ return null;
219
+ }
220
+
221
+ /** What the spotlight becomes: its query, the ids lit alongside, and the agent search that set it (`null`: the person's). */
222
+ export interface AgentSpotlight {
223
+ query: string;
224
+ ids: readonly string[];
225
+ agent: number | null;
226
+ }
227
+
228
+ /**
229
+ * How the graph view's spotlight follows the agent's searches, when
230
+ * {@link agentSearch} changes. `null` leaves the spotlight alone.
231
+ *
232
+ * A new search takes the spotlight over, the way the palette's "light up in
233
+ * graph" does, unless the person is in the middle of a query of their own:
234
+ * the field focused with words they typed. A focused field that is empty, or
235
+ * still shows an earlier agent search, is not theirs and follows along, so a
236
+ * field left focused never swallows a search. When the turn that searched is
237
+ * over, the spotlight it lit goes out, unless the person has made it theirs
238
+ * since by typing or lighting something up.
239
+ */
240
+ export function agentSpotlight(
241
+ /** The `seq` of the agent search the spotlight shows now, or `null` when it is the person's (or empty). */
242
+ shown: number | null,
243
+ search: AgentSearch | null,
244
+ /** Whether the field has focus and holds words; `shown` says whose words they are. */
245
+ composing: boolean,
246
+ ): AgentSpotlight | null {
247
+ if (search === null) return shown === null ? null : { query: "", ids: [], agent: null };
248
+ if (search.seq === shown || (composing && shown === null)) return null;
249
+ return { query: search.query, ids: search.ids, agent: search.seq };
195
250
  }