@mariozechner/pi-coding-agent 0.42.4 → 0.43.0

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 (112) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +14 -9
  3. package/dist/cli/list-models.d.ts.map +1 -1
  4. package/dist/cli/list-models.js +1 -1
  5. package/dist/cli/list-models.js.map +1 -1
  6. package/dist/cli/session-picker.d.ts +4 -2
  7. package/dist/cli/session-picker.d.ts.map +1 -1
  8. package/dist/cli/session-picker.js +3 -3
  9. package/dist/cli/session-picker.js.map +1 -1
  10. package/dist/core/agent-session.d.ts +14 -8
  11. package/dist/core/agent-session.d.ts.map +1 -1
  12. package/dist/core/agent-session.js +37 -15
  13. package/dist/core/agent-session.js.map +1 -1
  14. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  15. package/dist/core/compaction/branch-summarization.js +3 -1
  16. package/dist/core/compaction/branch-summarization.js.map +1 -1
  17. package/dist/core/extensions/index.d.ts +2 -2
  18. package/dist/core/extensions/index.d.ts.map +1 -1
  19. package/dist/core/extensions/index.js.map +1 -1
  20. package/dist/core/extensions/runner.d.ts +2 -2
  21. package/dist/core/extensions/runner.d.ts.map +1 -1
  22. package/dist/core/extensions/runner.js +9 -5
  23. package/dist/core/extensions/runner.js.map +1 -1
  24. package/dist/core/extensions/types.d.ts +25 -14
  25. package/dist/core/extensions/types.d.ts.map +1 -1
  26. package/dist/core/extensions/types.js.map +1 -1
  27. package/dist/core/footer-data-provider.d.ts.map +1 -1
  28. package/dist/core/footer-data-provider.js +10 -4
  29. package/dist/core/footer-data-provider.js.map +1 -1
  30. package/dist/core/index.d.ts +1 -1
  31. package/dist/core/index.d.ts.map +1 -1
  32. package/dist/core/index.js.map +1 -1
  33. package/dist/core/session-manager.d.ts +11 -2
  34. package/dist/core/session-manager.d.ts.map +1 -1
  35. package/dist/core/session-manager.js +142 -64
  36. package/dist/core/session-manager.js.map +1 -1
  37. package/dist/core/settings-manager.d.ts +7 -3
  38. package/dist/core/settings-manager.d.ts.map +1 -1
  39. package/dist/core/settings-manager.js +15 -0
  40. package/dist/core/settings-manager.js.map +1 -1
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/main.d.ts.map +1 -1
  45. package/dist/main.js +13 -12
  46. package/dist/main.js.map +1 -1
  47. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  48. package/dist/modes/interactive/components/extension-editor.js +8 -8
  49. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  50. package/dist/modes/interactive/components/index.d.ts +1 -0
  51. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  52. package/dist/modes/interactive/components/index.js +1 -0
  53. package/dist/modes/interactive/components/index.js.map +1 -1
  54. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  55. package/dist/modes/interactive/components/model-selector.js +1 -2
  56. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  57. package/dist/modes/interactive/components/scoped-models-selector.d.ts +47 -0
  58. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -0
  59. package/dist/modes/interactive/components/scoped-models-selector.js +241 -0
  60. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -0
  61. package/dist/modes/interactive/components/session-selector.d.ts +17 -3
  62. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  63. package/dist/modes/interactive/components/session-selector.js +167 -35
  64. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  65. package/dist/modes/interactive/components/settings-selector.d.ts +4 -2
  66. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  67. package/dist/modes/interactive/components/settings-selector.js +13 -1
  68. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  69. package/dist/modes/interactive/components/tree-selector.d.ts +2 -2
  70. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  71. package/dist/modes/interactive/components/tree-selector.js +8 -7
  72. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  73. package/dist/modes/interactive/interactive-mode.d.ts +6 -0
  74. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  75. package/dist/modes/interactive/interactive-mode.js +249 -37
  76. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  77. package/dist/modes/print-mode.d.ts.map +1 -1
  78. package/dist/modes/print-mode.js +2 -2
  79. package/dist/modes/print-mode.js.map +1 -1
  80. package/dist/modes/rpc/rpc-client.d.ts +4 -4
  81. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  82. package/dist/modes/rpc/rpc-client.js +6 -6
  83. package/dist/modes/rpc/rpc-client.js.map +1 -1
  84. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  85. package/dist/modes/rpc/rpc-mode.js +11 -8
  86. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  87. package/dist/modes/rpc/rpc-types.d.ts +4 -4
  88. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  89. package/dist/modes/rpc/rpc-types.js.map +1 -1
  90. package/docs/extensions.md +45 -12
  91. package/docs/rpc.md +10 -10
  92. package/docs/sdk.md +10 -5
  93. package/docs/session.md +1 -1
  94. package/docs/skills.md +27 -0
  95. package/docs/tree.md +9 -5
  96. package/docs/tui.md +2 -0
  97. package/examples/extensions/README.md +3 -3
  98. package/examples/extensions/confirm-destructive.ts +5 -5
  99. package/examples/extensions/dirty-repo-guard.ts +2 -2
  100. package/examples/extensions/git-checkpoint.ts +3 -3
  101. package/examples/extensions/handoff.ts +1 -1
  102. package/examples/extensions/model-status.ts +31 -0
  103. package/examples/extensions/todo.ts +1 -1
  104. package/examples/extensions/tools.ts +2 -2
  105. package/examples/extensions/with-deps/package-lock.json +2 -2
  106. package/examples/extensions/with-deps/package.json +1 -1
  107. package/examples/sdk/11-sessions.ts +1 -1
  108. package/package.json +4 -4
  109. package/dist/utils/fuzzy.d.ts +0 -7
  110. package/dist/utils/fuzzy.d.ts.map +0 -1
  111. package/dist/utils/fuzzy.js +0 -86
  112. package/dist/utils/fuzzy.js.map +0 -1
package/docs/tree.md CHANGED
@@ -6,14 +6,14 @@ The `/tree` command provides tree-based navigation of the session history.
6
6
 
7
7
  Sessions are stored as trees where each entry has an `id` and `parentId`. The "leaf" pointer tracks the current position. `/tree` lets you navigate to any point and optionally summarize the branch you're leaving.
8
8
 
9
- ### Comparison with `/branch`
9
+ ### Comparison with `/fork`
10
10
 
11
- | Feature | `/branch` | `/tree` |
12
- |---------|-----------|---------|
11
+ | Feature | `/fork` | `/tree` |
12
+ |---------|---------|---------|
13
13
  | View | Flat list of user messages | Full tree structure |
14
14
  | Action | Extracts path to **new session file** | Changes leaf in **same session** |
15
15
  | Summary | Never | Optional (user prompted) |
16
- | Events | `session_before_branch` / `session_branch` | `session_before_tree` / `session_tree` |
16
+ | Events | `session_before_fork` / `session_fork` | `session_before_tree` / `session_tree` |
17
17
 
18
18
  ## Tree UI
19
19
 
@@ -66,7 +66,11 @@ If user selects the very first message (has no parent):
66
66
 
67
67
  ## Branch Summarization
68
68
 
69
- When switching, user is prompted: "Summarize the branch you're leaving?"
69
+ When switching branches, user is presented with three options:
70
+
71
+ 1. **No summary** - Switch immediately without summarizing
72
+ 2. **Summarize** - Generate a summary using the default prompt
73
+ 3. **Summarize with custom prompt** - Opens an editor to enter additional focus instructions that are appended to the default summarization prompt
70
74
 
71
75
  ### What Gets Summarized
72
76
 
package/docs/tui.md CHANGED
@@ -24,6 +24,8 @@ interface Component {
24
24
  | `handleInput?(data)` | Receive keyboard input when component has focus. |
25
25
  | `invalidate?()` | Clear cached render state. |
26
26
 
27
+ The TUI appends a full SGR reset and OSC 8 reset at the end of each rendered line. Styles do not carry across lines. If you emit multi-line text with styling, reapply styles per line or use `wrapTextWithAnsi()` so styles are preserved for each wrapped line.
28
+
27
29
  ## Using Components
28
30
 
29
31
  **In hooks** via `ctx.ui.custom()`:
@@ -20,7 +20,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
20
20
  |-----------|-------------|
21
21
  | `permission-gate.ts` | Prompts for confirmation before dangerous bash commands (rm -rf, sudo, etc.) |
22
22
  | `protected-paths.ts` | Blocks writes to protected paths (.env, .git/, node_modules/) |
23
- | `confirm-destructive.ts` | Confirms before destructive session actions (clear, switch, branch) |
23
+ | `confirm-destructive.ts` | Confirms before destructive session actions (clear, switch, fork) |
24
24
  | `dirty-repo-guard.ts` | Prevents session changes with uncommitted git changes |
25
25
 
26
26
  ### Custom Tools
@@ -53,7 +53,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
53
53
 
54
54
  | Extension | Description |
55
55
  |-----------|-------------|
56
- | `git-checkpoint.ts` | Creates git stash checkpoints at each turn for code restoration on branch |
56
+ | `git-checkpoint.ts` | Creates git stash checkpoints at each turn for code restoration on fork |
57
57
  | `auto-commit-on-exit.ts` | Auto-commits on exit using last assistant message for commit message |
58
58
 
59
59
  ### System Prompt & Compaction
@@ -129,7 +129,7 @@ action: Type.Union([Type.Literal("list"), Type.Literal("add")])
129
129
 
130
130
  **State persistence via details:**
131
131
  ```typescript
132
- // Store state in tool result details for proper branching support
132
+ // Store state in tool result details for proper forking support
133
133
  return {
134
134
  content: [{ type: "text", text: "Done" }],
135
135
  details: { todos: [...todos], nextId }, // Persisted in session
@@ -43,16 +43,16 @@ export default function (pi: ExtensionAPI) {
43
43
  }
44
44
  });
45
45
 
46
- pi.on("session_before_branch", async (event, ctx) => {
46
+ pi.on("session_before_fork", async (event, ctx) => {
47
47
  if (!ctx.hasUI) return;
48
48
 
49
- const choice = await ctx.ui.select(`Branch from entry ${event.entryId.slice(0, 8)}?`, [
50
- "Yes, create branch",
49
+ const choice = await ctx.ui.select(`Fork from entry ${event.entryId.slice(0, 8)}?`, [
50
+ "Yes, create fork",
51
51
  "No, stay in current session",
52
52
  ]);
53
53
 
54
- if (choice !== "Yes, create branch") {
55
- ctx.ui.notify("Branch cancelled", "info");
54
+ if (choice !== "Yes, create fork") {
55
+ ctx.ui.notify("Fork cancelled", "info");
56
56
  return { cancel: true };
57
57
  }
58
58
  });
@@ -50,7 +50,7 @@ export default function (pi: ExtensionAPI) {
50
50
  return checkDirtyRepo(pi, ctx, action);
51
51
  });
52
52
 
53
- pi.on("session_before_branch", async (_event, ctx) => {
54
- return checkDirtyRepo(pi, ctx, "branch");
53
+ pi.on("session_before_fork", async (_event, ctx) => {
54
+ return checkDirtyRepo(pi, ctx, "fork");
55
55
  });
56
56
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Git Checkpoint Extension
3
3
  *
4
- * Creates git stash checkpoints at each turn so /branch can restore code state.
5
- * When branching, offers to restore code to that point in history.
4
+ * Creates git stash checkpoints at each turn so /fork can restore code state.
5
+ * When forking, offers to restore code to that point in history.
6
6
  */
7
7
 
8
8
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
@@ -26,7 +26,7 @@ export default function (pi: ExtensionAPI) {
26
26
  }
27
27
  });
28
28
 
29
- pi.on("session_before_branch", async (event, ctx) => {
29
+ pi.on("session_before_fork", async (event, ctx) => {
30
30
  const ref = checkpoints.get(event.entryId);
31
31
  if (!ref) return;
32
32
 
@@ -125,7 +125,7 @@ export default function (pi: ExtensionAPI) {
125
125
  }
126
126
 
127
127
  // Let user edit the generated prompt
128
- const editedPrompt = await ctx.ui.editor("Edit handoff prompt (ctrl+enter to submit, esc to cancel)", result);
128
+ const editedPrompt = await ctx.ui.editor("Edit handoff prompt", result);
129
129
 
130
130
  if (editedPrompt === undefined) {
131
131
  ctx.ui.notify("Cancelled", "info");
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Model status extension - shows model changes in the status bar.
3
+ *
4
+ * Demonstrates the `model_select` hook which fires when the model changes
5
+ * via /model command, Ctrl+P cycling, or session restore.
6
+ *
7
+ * Usage: pi -e ./model-status.ts
8
+ */
9
+
10
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
11
+
12
+ export default function (pi: ExtensionAPI) {
13
+ pi.on("model_select", async (event, ctx) => {
14
+ const { model, previousModel, source } = event;
15
+
16
+ // Format model identifiers
17
+ const next = `${model.provider}/${model.id}`;
18
+ const prev = previousModel ? `${previousModel.provider}/${previousModel.id}` : "none";
19
+
20
+ // Show notification on change
21
+ if (source !== "restore") {
22
+ ctx.ui.notify(`Model: ${next}`, "info");
23
+ }
24
+
25
+ // Update status bar with current model
26
+ ctx.ui.setStatus("model", `🤖 ${model.id}`);
27
+
28
+ // Log change details (visible in debug output)
29
+ console.log(`[model_select] ${prev} → ${next} (${source})`);
30
+ });
31
+ }
@@ -131,7 +131,7 @@ export default function (pi: ExtensionAPI) {
131
131
  // Reconstruct state on session events
132
132
  pi.on("session_start", async (_event, ctx) => reconstructState(ctx));
133
133
  pi.on("session_switch", async (_event, ctx) => reconstructState(ctx));
134
- pi.on("session_branch", async (_event, ctx) => reconstructState(ctx));
134
+ pi.on("session_fork", async (_event, ctx) => reconstructState(ctx));
135
135
  pi.on("session_tree", async (_event, ctx) => reconstructState(ctx));
136
136
 
137
137
  // Register the todo tool for the LLM
@@ -138,8 +138,8 @@ export default function toolsExtension(pi: ExtensionAPI) {
138
138
  restoreFromBranch(ctx);
139
139
  });
140
140
 
141
- // Restore state after branching
142
- pi.on("session_branch", async (_event, ctx) => {
141
+ // Restore state after forking
142
+ pi.on("session_fork", async (_event, ctx) => {
143
143
  restoreFromBranch(ctx);
144
144
  });
145
145
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pi-extension-with-deps",
3
- "version": "1.6.4",
3
+ "version": "1.7.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pi-extension-with-deps",
9
- "version": "1.6.4",
9
+ "version": "1.7.0",
10
10
  "dependencies": {
11
11
  "ms": "^2.1.3"
12
12
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-with-deps",
3
3
  "private": true,
4
- "version": "1.6.4",
4
+ "version": "1.7.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -26,7 +26,7 @@ if (modelFallbackMessage) console.log("Note:", modelFallbackMessage);
26
26
  console.log("Continued session:", continued.sessionFile);
27
27
 
28
28
  // List and open specific session
29
- const sessions = SessionManager.list(process.cwd());
29
+ const sessions = await SessionManager.list(process.cwd());
30
30
  console.log(`\nFound ${sessions.length} sessions:`);
31
31
  for (const info of sessions.slice(0, 3)) {
32
32
  console.log(` ${info.id.slice(0, 8)}... - "${info.firstMessage.slice(0, 30)}..."`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mariozechner/pi-coding-agent",
3
- "version": "0.42.4",
3
+ "version": "0.43.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -39,9 +39,9 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@mariozechner/clipboard": "^0.3.0",
42
- "@mariozechner/pi-agent-core": "^0.42.4",
43
- "@mariozechner/pi-ai": "^0.42.4",
44
- "@mariozechner/pi-tui": "^0.42.4",
42
+ "@mariozechner/pi-agent-core": "^0.43.0",
43
+ "@mariozechner/pi-ai": "^0.43.0",
44
+ "@mariozechner/pi-tui": "^0.43.0",
45
45
  "chalk": "^5.5.0",
46
46
  "cli-highlight": "^2.1.11",
47
47
  "diff": "^8.0.2",
@@ -1,7 +0,0 @@
1
- export interface FuzzyMatch {
2
- matches: boolean;
3
- score: number;
4
- }
5
- export declare function fuzzyMatch(query: string, text: string): FuzzyMatch;
6
- export declare function fuzzyFilter<T>(items: T[], query: string, getText: (item: T) => string): T[];
7
- //# sourceMappingURL=fuzzy.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"fuzzy.d.ts","sourceRoot":"","sources":["../../src/utils/fuzzy.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,UAAU;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,CAoDlE;AAID,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,CAAC,EAAE,CA2C3F","sourcesContent":["// Fuzzy search. Matches if all query characters appear in order (not necessarily consecutive).\n// Lower score = better match.\n\nexport interface FuzzyMatch {\n\tmatches: boolean;\n\tscore: number;\n}\n\nexport function fuzzyMatch(query: string, text: string): FuzzyMatch {\n\tconst queryLower = query.toLowerCase();\n\tconst textLower = text.toLowerCase();\n\n\tif (queryLower.length === 0) {\n\t\treturn { matches: true, score: 0 };\n\t}\n\n\tif (queryLower.length > textLower.length) {\n\t\treturn { matches: false, score: 0 };\n\t}\n\n\tlet queryIndex = 0;\n\tlet score = 0;\n\tlet lastMatchIndex = -1;\n\tlet consecutiveMatches = 0;\n\n\tfor (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) {\n\t\tif (textLower[i] === queryLower[queryIndex]) {\n\t\t\tconst isWordBoundary = i === 0 || /[\\s\\-_./]/.test(textLower[i - 1]!);\n\n\t\t\t// Reward consecutive character matches (e.g., typing \"foo\" matches \"foobar\" better than \"f_o_o\")\n\t\t\tif (lastMatchIndex === i - 1) {\n\t\t\t\tconsecutiveMatches++;\n\t\t\t\tscore -= consecutiveMatches * 5;\n\t\t\t} else {\n\t\t\t\tconsecutiveMatches = 0;\n\t\t\t\t// Penalize gaps between matched characters\n\t\t\t\tif (lastMatchIndex >= 0) {\n\t\t\t\t\tscore += (i - lastMatchIndex - 1) * 2;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Reward matches at word boundaries (start of words are more likely intentional targets)\n\t\t\tif (isWordBoundary) {\n\t\t\t\tscore -= 10;\n\t\t\t}\n\n\t\t\t// Slight penalty for matches later in the string (prefer earlier matches)\n\t\t\tscore += i * 0.1;\n\n\t\t\tlastMatchIndex = i;\n\t\t\tqueryIndex++;\n\t\t}\n\t}\n\n\t// Not all query characters were found in order\n\tif (queryIndex < queryLower.length) {\n\t\treturn { matches: false, score: 0 };\n\t}\n\n\treturn { matches: true, score };\n}\n\n// Filter and sort items by fuzzy match quality (best matches first)\n// Supports space-separated tokens: all tokens must match, sorted by match count then score\nexport function fuzzyFilter<T>(items: T[], query: string, getText: (item: T) => string): T[] {\n\tif (!query.trim()) {\n\t\treturn items;\n\t}\n\n\t// Split query into tokens\n\tconst tokens = query\n\t\t.trim()\n\t\t.split(/\\s+/)\n\t\t.filter((t) => t.length > 0);\n\n\tif (tokens.length === 0) {\n\t\treturn items;\n\t}\n\n\tconst results: { item: T; totalScore: number }[] = [];\n\n\tfor (const item of items) {\n\t\tconst text = getText(item);\n\t\tlet totalScore = 0;\n\t\tlet allMatch = true;\n\n\t\t// Check each token against the text - ALL must match\n\t\tfor (const token of tokens) {\n\t\t\tconst match = fuzzyMatch(token, text);\n\t\t\tif (match.matches) {\n\t\t\t\ttotalScore += match.score;\n\t\t\t} else {\n\t\t\t\tallMatch = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// Only include if all tokens match\n\t\tif (allMatch) {\n\t\t\tresults.push({ item, totalScore });\n\t\t}\n\t}\n\n\t// Sort by score (asc, lower is better)\n\tresults.sort((a, b) => a.totalScore - b.totalScore);\n\n\treturn results.map((r) => r.item);\n}\n"]}
@@ -1,86 +0,0 @@
1
- // Fuzzy search. Matches if all query characters appear in order (not necessarily consecutive).
2
- // Lower score = better match.
3
- export function fuzzyMatch(query, text) {
4
- const queryLower = query.toLowerCase();
5
- const textLower = text.toLowerCase();
6
- if (queryLower.length === 0) {
7
- return { matches: true, score: 0 };
8
- }
9
- if (queryLower.length > textLower.length) {
10
- return { matches: false, score: 0 };
11
- }
12
- let queryIndex = 0;
13
- let score = 0;
14
- let lastMatchIndex = -1;
15
- let consecutiveMatches = 0;
16
- for (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) {
17
- if (textLower[i] === queryLower[queryIndex]) {
18
- const isWordBoundary = i === 0 || /[\s\-_./]/.test(textLower[i - 1]);
19
- // Reward consecutive character matches (e.g., typing "foo" matches "foobar" better than "f_o_o")
20
- if (lastMatchIndex === i - 1) {
21
- consecutiveMatches++;
22
- score -= consecutiveMatches * 5;
23
- }
24
- else {
25
- consecutiveMatches = 0;
26
- // Penalize gaps between matched characters
27
- if (lastMatchIndex >= 0) {
28
- score += (i - lastMatchIndex - 1) * 2;
29
- }
30
- }
31
- // Reward matches at word boundaries (start of words are more likely intentional targets)
32
- if (isWordBoundary) {
33
- score -= 10;
34
- }
35
- // Slight penalty for matches later in the string (prefer earlier matches)
36
- score += i * 0.1;
37
- lastMatchIndex = i;
38
- queryIndex++;
39
- }
40
- }
41
- // Not all query characters were found in order
42
- if (queryIndex < queryLower.length) {
43
- return { matches: false, score: 0 };
44
- }
45
- return { matches: true, score };
46
- }
47
- // Filter and sort items by fuzzy match quality (best matches first)
48
- // Supports space-separated tokens: all tokens must match, sorted by match count then score
49
- export function fuzzyFilter(items, query, getText) {
50
- if (!query.trim()) {
51
- return items;
52
- }
53
- // Split query into tokens
54
- const tokens = query
55
- .trim()
56
- .split(/\s+/)
57
- .filter((t) => t.length > 0);
58
- if (tokens.length === 0) {
59
- return items;
60
- }
61
- const results = [];
62
- for (const item of items) {
63
- const text = getText(item);
64
- let totalScore = 0;
65
- let allMatch = true;
66
- // Check each token against the text - ALL must match
67
- for (const token of tokens) {
68
- const match = fuzzyMatch(token, text);
69
- if (match.matches) {
70
- totalScore += match.score;
71
- }
72
- else {
73
- allMatch = false;
74
- break;
75
- }
76
- }
77
- // Only include if all tokens match
78
- if (allMatch) {
79
- results.push({ item, totalScore });
80
- }
81
- }
82
- // Sort by score (asc, lower is better)
83
- results.sort((a, b) => a.totalScore - b.totalScore);
84
- return results.map((r) => r.item);
85
- }
86
- //# sourceMappingURL=fuzzy.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"fuzzy.js","sourceRoot":"","sources":["../../src/utils/fuzzy.ts"],"names":[],"mappings":"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","sourcesContent":["// Fuzzy search. Matches if all query characters appear in order (not necessarily consecutive).\n// Lower score = better match.\n\nexport interface FuzzyMatch {\n\tmatches: boolean;\n\tscore: number;\n}\n\nexport function fuzzyMatch(query: string, text: string): FuzzyMatch {\n\tconst queryLower = query.toLowerCase();\n\tconst textLower = text.toLowerCase();\n\n\tif (queryLower.length === 0) {\n\t\treturn { matches: true, score: 0 };\n\t}\n\n\tif (queryLower.length > textLower.length) {\n\t\treturn { matches: false, score: 0 };\n\t}\n\n\tlet queryIndex = 0;\n\tlet score = 0;\n\tlet lastMatchIndex = -1;\n\tlet consecutiveMatches = 0;\n\n\tfor (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) {\n\t\tif (textLower[i] === queryLower[queryIndex]) {\n\t\t\tconst isWordBoundary = i === 0 || /[\\s\\-_./]/.test(textLower[i - 1]!);\n\n\t\t\t// Reward consecutive character matches (e.g., typing \"foo\" matches \"foobar\" better than \"f_o_o\")\n\t\t\tif (lastMatchIndex === i - 1) {\n\t\t\t\tconsecutiveMatches++;\n\t\t\t\tscore -= consecutiveMatches * 5;\n\t\t\t} else {\n\t\t\t\tconsecutiveMatches = 0;\n\t\t\t\t// Penalize gaps between matched characters\n\t\t\t\tif (lastMatchIndex >= 0) {\n\t\t\t\t\tscore += (i - lastMatchIndex - 1) * 2;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Reward matches at word boundaries (start of words are more likely intentional targets)\n\t\t\tif (isWordBoundary) {\n\t\t\t\tscore -= 10;\n\t\t\t}\n\n\t\t\t// Slight penalty for matches later in the string (prefer earlier matches)\n\t\t\tscore += i * 0.1;\n\n\t\t\tlastMatchIndex = i;\n\t\t\tqueryIndex++;\n\t\t}\n\t}\n\n\t// Not all query characters were found in order\n\tif (queryIndex < queryLower.length) {\n\t\treturn { matches: false, score: 0 };\n\t}\n\n\treturn { matches: true, score };\n}\n\n// Filter and sort items by fuzzy match quality (best matches first)\n// Supports space-separated tokens: all tokens must match, sorted by match count then score\nexport function fuzzyFilter<T>(items: T[], query: string, getText: (item: T) => string): T[] {\n\tif (!query.trim()) {\n\t\treturn items;\n\t}\n\n\t// Split query into tokens\n\tconst tokens = query\n\t\t.trim()\n\t\t.split(/\\s+/)\n\t\t.filter((t) => t.length > 0);\n\n\tif (tokens.length === 0) {\n\t\treturn items;\n\t}\n\n\tconst results: { item: T; totalScore: number }[] = [];\n\n\tfor (const item of items) {\n\t\tconst text = getText(item);\n\t\tlet totalScore = 0;\n\t\tlet allMatch = true;\n\n\t\t// Check each token against the text - ALL must match\n\t\tfor (const token of tokens) {\n\t\t\tconst match = fuzzyMatch(token, text);\n\t\t\tif (match.matches) {\n\t\t\t\ttotalScore += match.score;\n\t\t\t} else {\n\t\t\t\tallMatch = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// Only include if all tokens match\n\t\tif (allMatch) {\n\t\t\tresults.push({ item, totalScore });\n\t\t}\n\t}\n\n\t// Sort by score (asc, lower is better)\n\tresults.sort((a, b) => a.totalScore - b.totalScore);\n\n\treturn results.map((r) => r.item);\n}\n"]}