Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
55197a8
Allow read-only Linear and GitHub MCP tools without prompts
claude Jul 23, 2026
6099e28
Scan production index in invalidation fan-out
claude Jul 23, 2026
5e884fe
Serve write-time content meta over indexed values in file-meta docs
claude Jul 23, 2026
b33adb3
Establish FileDef-consumer test baseline via dependency fan-out
claude Jul 23, 2026
1acbfb5
Tighten file-meta precedence comment to state the invariant
claude Jul 23, 2026
328e80b
Ignore a blank QUNIT_FILTER instead of running zero tests
claude Jul 23, 2026
ec16ba0
Consolidate Linear MCP allowlist to one canonical spelling
claude Jul 23, 2026
cffe2c2
Add /_client-telemetry ingest endpoint to the realm-server
habdelra Jul 23, 2026
bc836b9
Add always-on client performance telemetry to the host
habdelra Jul 23, 2026
754b7a1
Add Client Performance Grafana dashboard
habdelra Jul 23, 2026
702702e
Name table value columns and drop the redundant time column
habdelra Jul 23, 2026
eb2ba06
Add PR screenshots (stripped in follow-up)
habdelra Jul 23, 2026
5b3cee5
Strip PR screenshot assets
habdelra Jul 23, 2026
3ecc57d
Drop the full-reindex test that didn't fail without the fix
claude Jul 24, 2026
8bedde0
Address review: keepalive body budget, early size guard, session filter
habdelra Jul 24, 2026
092a739
Measure deserialize doc size from the response, not a re-serialize
habdelra Jul 24, 2026
bdd262d
Add refreshed By-user screenshot (stripped in follow-up)
habdelra Jul 24, 2026
d20791b
Strip refreshed By-user screenshot asset
habdelra Jul 24, 2026
27eeac2
Address review: scope card-load loads, corroborate wedges, guard prer…
habdelra Jul 24, 2026
c9eaa90
Address review: name dashboard value columns per query
habdelra Jul 24, 2026
e10fe04
Don't emit a spurious card-load when a backgrounded tab refocuses
habdelra Jul 24, 2026
a4f76e7
Require a Content-Length on telemetry beacons before buffering the body
habdelra Jul 24, 2026
8de9d7f
Re-check wedge corroboration across a grace window before dropping
habdelra Jul 24, 2026
88e06e2
Test the /_client-telemetry ingest handler
habdelra Jul 24, 2026
d4824a1
Persist search sheet state across close/reopen (#5534)
FadhlanR Jul 24, 2026
4fa919e
Update host test memory baseline [skip ci]
github-actions[bot] Jul 24, 2026
eabce4d
Merge pull request #5595 from cardstack/claude/cs-12172-evergreen-com…
habdelra Jul 24, 2026
c287ae9
Update host test memory baseline [skip ci]
github-actions[bot] Jul 24, 2026
a885dca
Merge remote-tracking branch 'origin/main' into cs-12288-client-perf-…
habdelra Jul 24, 2026
6276be4
Merge remote-tracking branch 'origin/flaky-base-realm-fetch-header-ti…
habdelra Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,23 @@
"Bash(node --check *)",
"Bash(npx vitest run *)",
"Bash(pnpm exec prettier *)",
"mcp__linear__list_comments",
"mcp__linear__get_issue",
"mcp__linear__get_project",
"mcp__linear__get_user",
"mcp__linear__list_projects",
"mcp__linear__list_issue_statuses",
"mcp__Linear__get_issue",
"mcp__Linear__list_comments",
"mcp__Linear__get_project",
"mcp__Linear__get_user",
"mcp__Linear__list_projects",
"mcp__Linear__list_issue_statuses",
"mcp__Linear__list_issues",
"mcp__github__pull_request_read",
"mcp__github__list_pull_requests",
"mcp__github__get_file_contents",
"mcp__github__search_code",
"mcp__github__list_commits",
"mcp__github__get_commit",
"mcp__github__get_me",
"mcp__github__actions_list",
"mcp__github__actions_get",
"mcp__github__get_job_logs",
"mcp__chrome-devtools__take_screenshot"
]
},
Expand Down
13 changes: 12 additions & 1 deletion packages/host/app/components/operator-mode/submode-layout.gts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import type AiAssistantPanelService from '../../services/ai-assistant-panel-serv
import type MatrixService from '../../services/matrix-service';
import type OperatorModeStateService from '../../services/operator-mode-state-service';
import type RecentCardsService from '../../services/recent-cards-service';
import type SearchSheetStateService from '../../services/search-sheet-state';
import type StoreService from '../../services/store';
import type { SearchSheetMode } from '../search-sheet';
import type { Submode } from '../submode-switcher';
Expand Down Expand Up @@ -137,6 +138,8 @@ export default class SubmodeLayout extends Component<Signature> {
@service declare private store: StoreService;
@service declare private aiAssistantPanelService: AiAssistantPanelService;
@service declare private recentCardsService: RecentCardsService;
@service('search-sheet-state')
declare private searchSheetState: SearchSheetStateService;

private searchElement: HTMLElement | null = null;
private suppressSearchClose = false;
Expand Down Expand Up @@ -346,7 +349,15 @@ export default class SubmodeLayout extends Component<Signature> {

@action private openSearchSheetToPrompt() {
if (this.searchSheetMode === SearchSheetModes.Closed) {
this.searchSheetMode = SearchSheetModes.SearchPrompt;
// Reopen straight to the results view when a search is persisted, so the
// restored results are shown immediately rather than the compact prompt.
// Gate on the service's own `hasActiveSearch` (term OR type OR realm) —
// the same predicate that produces `mainQuery` — so a filter-only search
// (e.g. code mode's "Find instances", which sets a type with no term)
// reopens to its live results rather than the recents-only compact prompt.
this.searchSheetMode = this.searchSheetState.hasActiveSearch
? SearchSheetModes.SearchResults
: SearchSheetModes.SearchPrompt;
}

this.searchElement?.focus();
Expand Down
192 changes: 132 additions & 60 deletions packages/host/app/components/search-sheet/index.gts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { array } from '@ember/helper';
import { on } from '@ember/modifier';
import { action } from '@ember/object';
import type Owner from '@ember/owner';
import { debounce } from '@ember/runloop';
import { cancel, debounce } from '@ember/runloop';
import { service } from '@ember/service';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

import onClickOutside from 'ember-click-outside/modifiers/on-click-outside';
import { modifier } from 'ember-modifier';
Expand All @@ -20,13 +20,11 @@ import {
import { eq } from '@cardstack/boxel-ui/helpers';
import { IconSearch } from '@cardstack/boxel-ui/icons';

import {
type Filter,
type ResolvedCodeRef,
baseRef,
} from '@cardstack/runtime-common';
import type { ResolvedCodeRef } from '@cardstack/runtime-common';

import type RealmServerService from '@cardstack/host/services/realm-server';
import type SearchSheetStateService from '@cardstack/host/services/search-sheet-state';
import { SEARCH_SHEET_BASE_FILTER } from '@cardstack/host/services/search-sheet-state';
import {
isURLSearchKey,
resolveSearchKeyAsURL,
Expand All @@ -35,6 +33,7 @@ import {
import SearchPanel from '../search/panel';

import type StoreService from '../../services/store';
import type { SortOption } from '../search/constants';

export const SearchSheetModes = {
Closed: 'closed',
Expand Down Expand Up @@ -79,19 +78,32 @@ const repositionDropdownsOnTransitionEnd = modifier((element: Element) => {
element.removeEventListener('transitionend', handler as EventListener);
});

// The sheet searches everything the index knows — cards (including specs,
// which the default `not: specRef` query path excludes), field instances, and
// files. BaseDef is the common ancestor stamped on both instance and file type
// chains, so this one ref spans all kinds; `scope: 'all'` rides the wire
// alongside (see `searchScopeForOptions`).
const BASE_FILTER: Filter = { type: baseRef };

export default class SearchSheet extends Component<Signature> {
@tracked private searchKey = '';
@tracked private initialSelectedTypes: ResolvedCodeRef[] | undefined;

@service declare private realmServer: RealmServerService;
@service declare private store: StoreService;
@service('search-sheet-state')
declare private searchSheetState: SearchSheetStateService;

// The sheet's search state is held in the session-scoped service so it
// survives the close/reopen that unmounts this component's subtree.
private get searchKey() {
return this.searchSheetState.searchKey;
}
private set searchKey(value: string) {
this.searchSheetState.searchKey = value;
}

private get initialSelectedTypes(): ResolvedCodeRef[] | undefined {
return this.searchSheetState.selectedTypes;
}

private get initialSelectedRealms(): URL[] {
return this.searchSheetState.selectedRealms;
}

private get initialActiveSort(): SortOption | undefined {
return this.searchSheetState.activeSort;
}

constructor(owner: Owner, args: any) {
super(owner, args);
Expand Down Expand Up @@ -141,33 +153,54 @@ export default class SearchSheet extends Component<Signature> {

@action
private onBlur() {
// A plain close/blur keeps the search so reopening restores it; only an
// explicit Cancel (or Escape) resets.
this.args.onBlur();
if (this.args.mode === SearchSheetModes.Closed) {
this.resetState();
}
}

@action private handleCardSelect(selection: string | { realmURL: string }) {
if (typeof selection !== 'string') {
return;
}
this.resetState();
// Selecting a result keeps the search, so reopening returns to it.
this.args.onCardSelect(selection);
}

@action
private doExternallyTriggeredSearch(term: string, typeRef?: ResolvedCodeRef) {
// A triggered search starts clean: leftover state from a previously
// persisted search (realm scope, sort, scroll offset, a pending debounce)
// must not silently narrow, position, or overwrite it. Reset everything,
// then apply the trigger's own term and type. Bumping the epoch remounts a
// `SearchPanel` that's already mounted (the sheet was open), so its
// init-once filter display re-reads the freshly-reset state instead of
// showing stale chips over the correctly-rescoped search.
this.resetState();
this.searchKey = term;
this.initialSelectedTypes = typeRef ? [typeRef] : undefined;
this.searchSheetState.selectedTypes = typeRef ? [typeRef] : undefined;
this.searchSheetState.searchTriggerEpoch++;
}

// The pending 300ms debounce handle, kept so Cancel/Escape can cancel it —
// otherwise a keystroke within 300ms of a reset re-persists the cancelled
// term and reopens the sheet.
private pendingSearchKeyDebounce: ReturnType<typeof debounce> | undefined;

private resetState() {
this.searchKey = '';
this.initialSelectedTypes = undefined;
if (this.pendingSearchKeyDebounce) {
cancel(this.pendingSearchKeyDebounce);
this.pendingSearchKeyDebounce = undefined;
}
this.searchSheetState.resetState();
}

@action private debouncedSetSearchKey(searchKey: string) {
debounce(this, this.setSearchKey, searchKey, 300);
this.pendingSearchKeyDebounce = debounce(
this,
this.setSearchKey,
searchKey,
300,
);
}

@action
Expand All @@ -176,14 +209,34 @@ export default class SearchSheet extends Component<Signature> {
this.args.onSearch?.(searchKey);
}

@action private handleRealmChange(_selectedRealms: URL[]) {
@action private handleRealmChange(selectedRealms: URL[]) {
this.searchSheetState.selectedRealms = selectedRealms;
this.args.onFilterChange?.();
}

@action private handleTypeChange(_selectedTypes: ResolvedCodeRef[]) {
@action private handleTypeChange(selectedTypes: ResolvedCodeRef[]) {
this.searchSheetState.selectedTypes = selectedTypes;
this.args.onFilterChange?.();
}

@action private handleSortChange(option: SortOption) {
// Unlike realm/type changes, no `onFilterChange` here: the sort control only
// exists in the results view, so there's never a prompt→results expansion to
// trigger — just record the choice for persistence.
this.searchSheetState.activeSort = option;
}

// The panel is a controlled consumer here: it renders the session-scoped
// service's view id / scroll offset and reports changes back through these,
// so both survive the sheet's close/reopen.
@action private handleViewIdChange(id: string) {
this.searchSheetState.activeViewId = id;
}

@action private handleScrollTopChange(scrollTop: number) {
this.searchSheetState.resultsScrollTop = scrollTop;
}

@action private onSearchInputKeyDown(e: Event) {
let kbEvent = e as KeyboardEvent;
if (kbEvent.key === 'Escape') {
Expand Down Expand Up @@ -264,40 +317,59 @@ export default class SearchSheet extends Component<Signature> {
data-test-open-search-field
/>
{{else}}
<SearchPanel
@searchKey={{this.searchKey}}
@baseFilter={{BASE_FILTER}}
@initialSelectedTypes={{this.initialSelectedTypes}}
@onRealmChange={{this.handleRealmChange}}
@onTypeChange={{this.handleTypeChange}}
as |Bar Content|
>
<Bar
class='search-sheet__search-input-group'
@placeholder={{this.placeholderText}}
@state={{this.inputValidationState}}
@bottomTreatment={{this.inputBottomTreatment}}
@onFocus={{@onFocus}}
@onInput={{this.debouncedSetSearchKey}}
@onKeyDown={{this.onSearchInputKeyDown}}
@onInputInsertion={{@onInputInsertion}}
@autocomplete='off'
/>
<Content
class='search-sheet__content'
@isCompact={{this.isCompact}}
@handleSelect={{this.handleCardSelect}}
@adorn={{true}}
/>
<div class='footer'>
<div class='buttons'>
<Button
{{on 'click' this.onCancel}}
data-test-search-sheet-cancel-button
>Cancel</Button>
{{! Keyed on the trigger epoch so an externally-triggered search (which
bumps it) remounts the panel — the only way its init-once filter
display picks up the freshly-reset state while the sheet is already
open. A normal reopen doesn't bump the epoch, so it reuses the
panel. }}
{{#each
(array this.searchSheetState.searchTriggerEpoch) key='@identity'
as |_epoch|
}}
<SearchPanel
@searchKey={{this.searchKey}}
@baseFilter={{SEARCH_SHEET_BASE_FILTER}}
@initialSelectedTypes={{this.initialSelectedTypes}}
@initialSelectedRealms={{this.initialSelectedRealms}}
@initialActiveSort={{this.initialActiveSort}}
@onRealmChange={{this.handleRealmChange}}
@onTypeChange={{this.handleTypeChange}}
@onSortChange={{this.handleSortChange}}
as |Bar Content|
>
<Bar
class='search-sheet__search-input-group'
@placeholder={{this.placeholderText}}
@state={{this.inputValidationState}}
@bottomTreatment={{this.inputBottomTreatment}}
@onFocus={{@onFocus}}
@onInput={{this.debouncedSetSearchKey}}
@onKeyDown={{this.onSearchInputKeyDown}}
@onInputInsertion={{@onInputInsertion}}
@autocomplete='off'
/>
<Content
class='search-sheet__content'
@isCompact={{this.isCompact}}
@handleSelect={{this.handleCardSelect}}
@adorn={{true}}
@mainSearchResource={{this.searchSheetState.mainSearch}}
@viewId={{this.searchSheetState.activeViewId}}
@onViewIdChange={{this.handleViewIdChange}}
@pagination={{this.searchSheetState.pagination}}
@scrollTop={{this.searchSheetState.resultsScrollTop}}
@onScrollTopChange={{this.handleScrollTopChange}}
/>
<div class='footer'>
<div class='buttons'>
<Button
{{on 'click' this.onCancel}}
data-test-search-sheet-cancel-button
>Cancel</Button>
</div>
</div>
</div>
</SearchPanel>
</SearchPanel>
{{/each}}
{{/if}}
</div>
<style scoped>
Expand Down
Loading
Loading