fKKOWvwO98|C&X-tL$5kdZO-w$@7QO|z^GmmXA4vw__{bXp1i
z-5J!rq@%XP{*7-6#kCDjUQ}cs7*=q$AC(m}w>z*SZjOaSO+YB^v1Dbl424S7(Pgs5
zbcg9!gt&J>p-o7Oj!1t`s8rragie;77f}s!R^kzD?_amaDbKJ+8C#~J0VC3CWrVTE
zc);|s1WkGUf2c&C 2fhw&ly-Bgx*;(yoJAO9S_0k{iV2c>
zA17LdENejIT0L5BGFQWtWFNXjACWAc(109p+qp#lN&YeTtrB}%blSY!wmzaI3sBOz
zP1%PIqALmcaFm+n*12pjnRfdA8(f8ZaYxvBKM~nwb$i$O+~D^$(~Yh0pOT}iR3dlV
zel`I&qSY9;q#)jsnis7dqFBYn?8Ub3+b?BGs0w(J&8J*8xZvBCKAZkgwLbX?1j8@Q
zhvn*Zbgz&HPzO!)2tM%ks&|GAxTvL)U`=mo ;%FqU-mr6F
ztCt}u&S0h~hx0YJeP6tx-n2)eX}e6lS@M?%WuzztB?$mo7Kws4U{bMc8);5K*(Kd7
z3U;n0w}%1BHo-sgkA=b@oHW56+6nH3((+3eiHTFVcMvH}`+30#Mj19K5&oH{@&c9D
zg~higJm{uIW&|l>9+P%-W#lX7I&9yYyPaQN7zAvou%J+Te!v?W!N@~J|Jiat&T;SG
zC6`08Gm%@PQBVD(aJ&QoAar8hH5NlqI}E$hls0zNtJ_&biV*1}AdGJUFG4)OEpehT
z?!o9Uo^w;k!WDG;=0RETT6^+ setFilter(value as SearchCategoryFilter)}
+ storeValue
+ >
+
+ );
+}
diff --git a/extensions/blume-search/src/searchLifecycle.test.ts b/extensions/blume-search/src/searchLifecycle.test.ts
new file mode 100644
index 00000000000..eb361dafb4f
--- /dev/null
+++ b/extensions/blume-search/src/searchLifecycle.test.ts
@@ -0,0 +1,12 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { searchStateForApplicationChange } from "./searchLifecycle.ts";
+
+test("switching Blume applications clears stale results and returns to loading", () => {
+ assert.deepEqual(searchStateForApplicationChange(), {
+ results: [],
+ isLoading: true,
+ error: null,
+ });
+});
diff --git a/extensions/blume-search/src/searchLifecycle.ts b/extensions/blume-search/src/searchLifecycle.ts
new file mode 100644
index 00000000000..290af964534
--- /dev/null
+++ b/extensions/blume-search/src/searchLifecycle.ts
@@ -0,0 +1,11 @@
+import type { GlobalSearchResult } from "./protocol.ts";
+
+export interface SearchState {
+ results: GlobalSearchResult[];
+ isLoading: boolean;
+ error: string | null;
+}
+
+export function searchStateForApplicationChange(): SearchState {
+ return { results: [], isLoading: true, error: null };
+}
diff --git a/extensions/blume-search/src/searchModel.test.ts b/extensions/blume-search/src/searchModel.test.ts
new file mode 100644
index 00000000000..1e5097dc625
--- /dev/null
+++ b/extensions/blume-search/src/searchModel.test.ts
@@ -0,0 +1,47 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import type { GlobalSearchResult } from "./protocol.ts";
+
+import { ALL_SEARCH_CATEGORIES, blumeDeepLinkForResult, categoriesForFilter, resultSubtitle } from "./searchModel.ts";
+
+test("Raycast searches every category through the shared global-search contract", () => {
+ assert.deepEqual(categoriesForFilter("all"), ALL_SEARCH_CATEGORIES);
+ assert.deepEqual(categoriesForFilter("messages"), ["messages"]);
+});
+
+test("search results map to the existing Blume deep-link routes", () => {
+ const conversation: GlobalSearchResult = {
+ id: "conversation-1",
+ category: "conversations",
+ title: "Search architecture",
+ subtitle: "Blume",
+ excerpt: null,
+ rank: 0,
+ updatedAt: 1,
+ conversationRef: "codex/project/thread",
+ conversationId: "conversation-1",
+ projectId: "project-1",
+ workspaceLocationId: null,
+ harnessId: "codex",
+ };
+ const conversationLink = blumeDeepLinkForResult(conversation);
+ assert.equal(conversationLink, "blume://agents/codex%2Fproject%2Fthread");
+ assert.equal(blumeDeepLinkForResult(conversation, "blume-canary"), "blume-canary://agents/codex%2Fproject%2Fthread");
+
+ const setup: GlobalSearchResult = {
+ id: "setup-1",
+ category: "setup",
+ title: "Review skill",
+ subtitle: null,
+ excerpt: "Review every change",
+ rank: 0,
+ updatedAt: 1,
+ artifactId: "setup-1",
+ projectId: "project-1",
+ harnessId: "codex",
+ sourcePath: "/code/.agents/skills/review/SKILL.md",
+ };
+ assert.equal(blumeDeepLinkForResult(setup), "blume://setup/codex/entries/SKILL.md");
+ assert.equal(resultSubtitle(setup), "Review every change");
+});
diff --git a/extensions/blume-search/src/searchModel.ts b/extensions/blume-search/src/searchModel.ts
new file mode 100644
index 00000000000..6f4dd10875e
--- /dev/null
+++ b/extensions/blume-search/src/searchModel.ts
@@ -0,0 +1,61 @@
+import { basename } from "node:path";
+
+import { GLOBAL_SEARCH_CATEGORIES, type GlobalSearchCategory, type GlobalSearchResult } from "./protocol.ts";
+
+export const ALL_SEARCH_CATEGORIES = [...GLOBAL_SEARCH_CATEGORIES];
+export type SearchCategoryFilter = "all" | GlobalSearchCategory;
+
+export const SEARCH_CATEGORY_LABELS: Record