Skip to content

Commit 9cb475a

Browse files
wu-shengclaude
andcommitted
feat(debug-lal): search filter + 20-record default cap with show-more knob
A busy LAL capture lands many records (one log line per column) — at ~30+ columns the matrix gets unscannable. Two new affordances: - Subhead `search` field. Free-text filter; matches when ANY sample in the record carries the substring (case-insensitive) in: - input LogData body.text - output LogBuilder.content - any LogBuilder tag key/value Empty query passes everything through. - Subhead `show first` numeric input (default 20, max 100). Caps the rendered column count after the search filter runs. - Header label cell now reads `showing N of M · K captured` so the operator sees how many got hidden by the cap and the search. - Empty-match state renders an italic "no records match <query>" hint instead of an empty grid. Implementation: - New `searchQuery` + `displayLimit` refs on the view. - `recordMatches(rec, q)` walks the record's samples once; covers all three text surfaces. `displayedRecords(view)` filters + slices, used by every iterator in both the matrix and the single-record expand views (so search and limit honour both modes). - The cells map is keyed by recIdx and stays intact — filtering only trims `recordViews`, so cellAt lookups for the surviving records remain O(1). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f2a1f6e commit 9cb475a

1 file changed

Lines changed: 177 additions & 8 deletions

File tree

apps/ui/src/views/debug/DebugLal.vue

Lines changed: 177 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,65 @@ function expandedRecordView(view: LalNodeView): LalRecordView | null {
433433
return view.recordViews.find((r) => r.recIdx === e.recIdx) ?? null;
434434
}
435435
436+
// ── Search + display limit ────────────────────────────────────────
437+
438+
/** Free-text filter on log content. A record matches when ANY of its
439+
* samples carry text containing the substring (case-insensitive):
440+
* - input LogData body.text
441+
* - output LogBuilder.content
442+
* - any LogBuilder tag key/value
443+
* Empty query matches everything. */
444+
const searchQuery = ref<string>('');
445+
/** UI-side cap so a busy capture doesn't render hundreds of columns
446+
* at once; operators raise this when they want the full set. */
447+
const displayLimit = ref<number>(20);
448+
449+
function recordMatches(rec: SessionRecord, q: string): boolean {
450+
if (q === '') return true;
451+
const needle = q.toLowerCase();
452+
for (const sample of rec.samples ?? []) {
453+
if (!isLalSamplePayload(sample.payload)) continue;
454+
const p = sample.payload;
455+
if (p.input?.type === 'LogData') {
456+
const body = (p.input as LalLogDataInput).body;
457+
const text = body?.text;
458+
if (typeof text === 'string' && text.toLowerCase().includes(needle)) return true;
459+
}
460+
if (p.output?.type === 'LogBuilder') {
461+
const out = p.output as LalLogBuilderOutput;
462+
if (typeof out.content === 'string' && out.content.toLowerCase().includes(needle)) {
463+
return true;
464+
}
465+
for (const t of out.tags ?? []) {
466+
if (
467+
t.key.toLowerCase().includes(needle) ||
468+
t.value.toLowerCase().includes(needle)
469+
) {
470+
return true;
471+
}
472+
}
473+
}
474+
}
475+
return false;
476+
}
477+
478+
/** Per-node post-filter view. Keeps the cells map intact (lookups
479+
* are by recIdx and the surviving recordViews carry their original
480+
* ids), trims `recordViews` to matching + capped subset. */
481+
function displayedRecords(view: LalNodeView): LalRecordView[] {
482+
const q = searchQuery.value.trim();
483+
const matched = q === ''
484+
? view.recordViews
485+
: view.recordViews.filter((rv) => recordMatches(rv.rec, q));
486+
return matched.slice(0, displayLimit.value);
487+
}
488+
489+
function matchedRecordCount(view: LalNodeView): number {
490+
const q = searchQuery.value.trim();
491+
if (q === '') return view.recordViews.length;
492+
return view.recordViews.filter((rv) => recordMatches(rv.rec, q)).length;
493+
}
494+
436495
const sourceDsl = computed<string | null>(() => {
437496
const sel = selectedCell.value;
438497
if (sel) return sel.rec.dsl ?? null;
@@ -578,6 +637,30 @@ void TAG_STATUS_TONE;
578637
</div>
579638
</template>
580639

640+
<template #subhead>
641+
<div class="lal__subhead">
642+
<label class="lal__searchwrap">
643+
<span class="lal__searchlbl">search</span>
644+
<input
645+
v-model="searchQuery"
646+
type="search"
647+
class="lal__searchinput"
648+
placeholder="filter records by log content / tag…"
649+
/>
650+
</label>
651+
<label class="lal__limitwrap">
652+
<span class="lal__searchlbl">show first</span>
653+
<input
654+
v-model.number="displayLimit"
655+
type="number"
656+
min="1"
657+
max="100"
658+
class="lal__limitinput"
659+
/>
660+
</label>
661+
</div>
662+
</template>
663+
581664
<template #idle-hint>
582665
pick a LAL rule and hit start. each captured log becomes one
583666
column in the matrix; rows walk the per-record blocks
@@ -692,13 +775,34 @@ void TAG_STATUS_TONE;
692775
<!-- Default: per-record × per-block matrix. -->
693776
<div v-else class="lal__matrixwrap">
694777
<div
778+
v-if="displayedRecords(node).length === 0"
779+
class="lal__nomatch"
780+
>
781+
<template v-if="searchQuery.trim() === ''">no records on this node</template>
782+
<template v-else>
783+
no records match
784+
<code>{{ searchQuery }}</code>
785+
({{ node.recordViews.length }} captured total)
786+
</template>
787+
</div>
788+
<div
789+
v-else
695790
class="lal__matrix"
696-
:style="`grid-template-columns: 180px repeat(${node.recordViews.length}, minmax(200px, 1fr));`"
791+
:style="`grid-template-columns: 180px repeat(${displayedRecords(node).length}, minmax(200px, 1fr));`"
697792
>
698793
<!-- header row: blank label cell + record headers -->
699-
<div class="lal__hdrlbl">block ▾ / record →</div>
794+
<div class="lal__hdrlbl">
795+
block ▾ / record →
796+
<div class="lal__hdrlblct">
797+
showing {{ displayedRecords(node).length }}
798+
of {{ matchedRecordCount(node) }}
799+
<span v-if="matchedRecordCount(node) !== node.recordViews.length">
800+
· {{ node.recordViews.length }} captured
801+
</span>
802+
</div>
803+
</div>
700804
<div
701-
v-for="rv in node.recordViews"
805+
v-for="rv in displayedRecords(node)"
702806
:key="`${nodeKey(node)}-h-${rv.recIdx}`"
703807
class="lal__hdrec"
704808
:class="{
@@ -722,14 +826,12 @@ void TAG_STATUS_TONE;
722826
<div class="lal__steplbl">
723827
<div class="lal__stepkind">{{ step.label }}</div>
724828
<div class="lal__stepct">
725-
{{
726-
Object.keys(Object.fromEntries([...node.cells.get(step.key)?.entries() ?? []])).length
727-
}}
728-
/ {{ node.recordViews.length }} records
829+
{{ displayedRecords(node).filter((rv) => cellAt(node, step, rv.recIdx) !== undefined).length }}
830+
/ {{ displayedRecords(node).length }} records
729831
</div>
730832
</div>
731833
<div
732-
v-for="rv in node.recordViews"
834+
v-for="rv in displayedRecords(node)"
733835
:key="`${step.key}-${rv.recIdx}`"
734836
class="lal__cell"
735837
:class="{
@@ -901,6 +1003,73 @@ void TAG_STATUS_TONE;
9011003
text-transform: uppercase;
9021004
}
9031005
1006+
.lal__hdrlblct {
1007+
margin-top: 4px;
1008+
font-family: var(--rr-font-mono);
1009+
font-size: 10px;
1010+
letter-spacing: 0;
1011+
text-transform: none;
1012+
color: var(--rr-ink2);
1013+
}
1014+
1015+
.lal__subhead {
1016+
display: flex;
1017+
align-items: center;
1018+
gap: 14px;
1019+
flex-wrap: wrap;
1020+
padding: 4px 0;
1021+
}
1022+
1023+
.lal__searchwrap,
1024+
.lal__limitwrap {
1025+
display: flex;
1026+
align-items: center;
1027+
gap: 6px;
1028+
font-family: var(--rr-font-mono);
1029+
}
1030+
1031+
.lal__searchlbl {
1032+
font-size: 11px;
1033+
letter-spacing: 0.6px;
1034+
text-transform: uppercase;
1035+
color: var(--rr-dim);
1036+
}
1037+
1038+
.lal__searchinput,
1039+
.lal__limitinput {
1040+
background: var(--rr-bg2);
1041+
color: var(--rr-ink);
1042+
border: 1px solid var(--rr-border);
1043+
padding: 3px 8px;
1044+
font-family: var(--rr-font-mono);
1045+
font-size: 13px;
1046+
}
1047+
1048+
.lal__searchinput {
1049+
min-width: 320px;
1050+
}
1051+
1052+
.lal__limitinput {
1053+
width: 60px;
1054+
}
1055+
1056+
.lal__nomatch {
1057+
padding: 24px 18px;
1058+
font-family: var(--rr-font-mono);
1059+
font-size: 13px;
1060+
color: var(--rr-dim);
1061+
text-align: center;
1062+
font-style: italic;
1063+
}
1064+
1065+
.lal__nomatch code {
1066+
font-family: var(--rr-font-mono);
1067+
background: var(--rr-bg2);
1068+
padding: 1px 5px;
1069+
font-style: normal;
1070+
color: var(--rr-ink);
1071+
}
1072+
9041073
.lal__hdrec {
9051074
position: sticky;
9061075
top: 0;

0 commit comments

Comments
 (0)