@oh-my-pi/pi-tui 18.2.6 → 18.2.7

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 (43) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/THIRD-PARTY-NOTICES.txt +0 -37
  3. package/dist/types/glyph-protocol.d.ts +62 -0
  4. package/dist/types/index.d.ts +1 -0
  5. package/dist/types/overlays/model-browser.d.ts +9 -3
  6. package/dist/types/prompt/gradient-highlight.d.ts +2 -2
  7. package/dist/types/prompt/magic-keywords.d.ts +37 -10
  8. package/dist/types/setup/scenes/glyph.d.ts +6 -1
  9. package/dist/types/terminal-capabilities.d.ts +8 -0
  10. package/dist/types/terminal.d.ts +16 -0
  11. package/dist/types/theme/mermaid-cache.d.ts +2 -2
  12. package/dist/types/tools/eval.d.ts +5 -4
  13. package/dist/types/tools/find.d.ts +82 -0
  14. package/dist/types/tools/web-search.d.ts +4 -0
  15. package/package.json +9 -9
  16. package/src/glyph-protocol.ts +143 -0
  17. package/src/index.ts +2 -0
  18. package/src/overlays/model-browser.ts +47 -6
  19. package/src/overlays/model-hub.ts +122 -33
  20. package/src/prompt/gradient-highlight.ts +3 -3
  21. package/src/prompt/macos-spelling.ts +7 -1
  22. package/src/prompt/magic-keywords.ts +103 -21
  23. package/src/prompt/markdown-prose.ts +2 -2
  24. package/src/prompt/tips.txt +1 -0
  25. package/src/setup/scenes/glyph.ts +8 -1
  26. package/src/terminal-capabilities.ts +12 -0
  27. package/src/terminal.ts +151 -2
  28. package/src/theme/glyph-bundle.json +797 -0
  29. package/src/theme/mermaid-cache.ts +5 -4
  30. package/src/tools/eval.ts +14 -7
  31. package/src/tools/find.ts +255 -0
  32. package/src/tools/hub.ts +2 -0
  33. package/src/tools/index.ts +2 -0
  34. package/src/tools/web-search.ts +5 -0
  35. package/src/tui.ts +7 -0
  36. package/dist/types/prompt/magic-keyword-boundary.d.ts +0 -9
  37. package/dist/types/prompt/orchestrate.d.ts +0 -13
  38. package/dist/types/prompt/ultrathink.d.ts +0 -13
  39. package/dist/types/prompt/workflow.d.ts +0 -13
  40. package/src/prompt/magic-keyword-boundary.ts +0 -23
  41. package/src/prompt/orchestrate.ts +0 -26
  42. package/src/prompt/ultrathink.ts +0 -26
  43. package/src/prompt/workflow.ts +0 -26
@@ -1,11 +1,12 @@
1
- import { type MermaidAsciiRenderOptions, renderMermaidAsciiSafe } from "@oh-my-pi/pi-utils/mermaid-ascii";
1
+ import type { MermaidRenderOptions } from "@oh-my-pi/pi-natives";
2
+ import { renderMermaidAsciiSafe } from "@oh-my-pi/pi-utils/mermaid-ascii";
2
3
 
3
4
  /**
4
5
  * Options controlling how fenced Mermaid source is resolved to terminal ASCII.
5
6
  * Extends the raw render options (theme, color mode, spacing, `useAscii`) with a
6
7
  * viewport-fitting hint.
7
8
  */
8
- export interface MermaidResolveOptions extends MermaidAsciiRenderOptions {
9
+ export interface MermaidResolveOptions extends MermaidRenderOptions {
9
10
  /**
10
11
  * Maximum display width (terminal columns) the diagram should occupy. A
11
12
  * layout that overflows this width is re-rendered in the perpendicular
@@ -34,7 +35,7 @@ function asciiDisplayWidth(ascii: string): number {
34
35
 
35
36
  function renderVariant(
36
37
  source: string,
37
- baseOptions: MermaidAsciiRenderOptions,
38
+ baseOptions: MermaidRenderOptions,
38
39
  baseKey: string,
39
40
  direction: "TD" | "LR" | null,
40
41
  ): string | null {
@@ -57,7 +58,7 @@ export function resolveMermaidAscii(source: string, options?: MermaidResolveOpti
57
58
 
58
59
  const { maxWidth, ...rest } = options ?? {};
59
60
  // Default to uncolored output; callers opt into a themed palette explicitly.
60
- const baseOptions: MermaidAsciiRenderOptions = { colorMode: "none", ...rest };
61
+ const baseOptions: MermaidRenderOptions = { colorMode: "none", ...rest };
61
62
  const baseKey = JSON.stringify(baseOptions);
62
63
 
63
64
  const base = renderVariant(normalizedSource, baseOptions, baseKey, null);
package/src/tools/eval.ts CHANGED
@@ -139,15 +139,16 @@ function getRenderCells(args: EvalRenderArgs | undefined): EvalRenderCell[] {
139
139
  type AgentEventStatus = "pending" | "running" | "completed" | "failed" | "aborted";
140
140
 
141
141
  /**
142
- * Append or replace a status event. `agent` events are progress snapshots keyed
143
- * by `id`, so they coalesce in place (preserving first-seen order); every other
144
- * op is a discrete action and simply appends. Keeps the persisted event list
145
- * bounded even when a subagent emits hundreds of throttled progress ticks.
142
+ * Append or replace a status event. `agent` and `judge_batch` events are
143
+ * progress snapshots keyed by `id`, so they coalesce in place (preserving
144
+ * first-seen order); every other op is a discrete action and simply appends.
145
+ * Keeps the persisted event list bounded even when a subagent or batch emits
146
+ * hundreds of throttled progress ticks.
146
147
  */
147
148
  export function upsertStatusEvent(events: EvalStatusEvent[], event: EvalStatusEvent): void {
148
- if (event.op === "agent" && typeof event.id === "string") {
149
- const id = event.id;
150
- const idx = events.findIndex(e => e.op === "agent" && e.id === id);
149
+ if ((event.op === "agent" || event.op === "judge_batch") && typeof event.id === "string") {
150
+ const { op, id } = event;
151
+ const idx = events.findIndex(e => e.op === op && e.id === id);
151
152
  if (idx >= 0) {
152
153
  events[idx] = event;
153
154
  return;
@@ -351,6 +352,12 @@ function formatStatusEvent(event: EvalStatusEvent, theme: Theme): string {
351
352
  parts.push(data.action === "create" ? `${data.count} agent(s)` : `${data.count} item(s)`);
352
353
  }
353
354
  break;
355
+ case "judge_batch":
356
+ parts.push(`${data.action} ${data.id}`);
357
+ parts.push(`${data.done ?? 0}/${data.total ?? 0}`);
358
+ if (data.failed) parts.push(`${data.failed} failed`);
359
+ if (data.model) parts.push(String(data.model));
360
+ break;
354
361
  case "wc":
355
362
  parts.push(`${data.lines}L ${data.words}W ${data.chars}C`);
356
363
  break;
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Transcript renderer for the `find` tool (semantic grep). Hits are ranked
3
+ * strongest first; each row carries a score gauge, the hyperlinked file, and
4
+ * its strongest verified line ranges with a verbatim snippet.
5
+ */
6
+ import * as path from "node:path";
7
+ import { formatDuration, formatNumber } from "@oh-my-pi/pi-utils";
8
+ import { renderProgressBar } from "../components/progress-bar";
9
+ import { Text } from "../components/text";
10
+ import { Ellipsis, fileHyperlink, getTreeBranch, renderStatusLine, renderTreeList, truncateToWidth } from "../render";
11
+ import {
12
+ createCachedComponent,
13
+ formatCount,
14
+ formatEmptyMessage,
15
+ formatErrorMessage,
16
+ PREVIEW_LIMITS,
17
+ replaceTabs,
18
+ } from "../render/render-utils";
19
+ import type { Theme, ThemeColor } from "../theme/theme";
20
+ import type { Component } from "../tui";
21
+ import type { OutputMeta } from "./output-meta";
22
+ import type { RenderResultOptions, ToolRenderer } from "./renderer";
23
+
24
+ /** A verified line range with its yes-probability and a one-line preview. */
25
+ export interface FindRange {
26
+ start: number;
27
+ end: number;
28
+ p: number;
29
+ snippet: string;
30
+ }
31
+
32
+ /** A file whose verified passages cleared the threshold; `ranges` are merged positive spans, strongest first. */
33
+ export interface FindHit {
34
+ /** Display path relative to {@link FindToolDetails.cwd}. */
35
+ rel: string;
36
+ /** Filename judgment, when the name batch answered. */
37
+ nameScore?: number;
38
+ /** Best verified passage probability. */
39
+ contentScore: number;
40
+ ranges: FindRange[];
41
+ /** Lines of content actually judged, and whether the file held more. */
42
+ linesSeen: number;
43
+ truncated: boolean;
44
+ }
45
+
46
+ /** Search accounting reported alongside the hits. */
47
+ export interface FindStats {
48
+ /** Eligible files under the root. */
49
+ listed: number;
50
+ requests: number;
51
+ errors: number;
52
+ /** Entries judged by name. */
53
+ judged: number;
54
+ /** Files whose content was read and sent. */
55
+ filesRead: number;
56
+ fileBytes: number;
57
+ inputTokens: number;
58
+ outputTokens: number;
59
+ cost: number;
60
+ apiMs: number;
61
+ windowsJudged: number;
62
+ windowsPruned: number;
63
+ mapCards: number;
64
+ /** Distinct request failures, phase-prefixed. */
65
+ failures: string[];
66
+ }
67
+
68
+ /** Display metadata for find tool results. */
69
+ export interface FindToolDetails {
70
+ query: string;
71
+ /** Lexical keywords actually used: derived from the query plus `grep_keywords`. */
72
+ keywords: string[];
73
+ /** Verified-passage probability at or above which a file is a hit. */
74
+ threshold: number;
75
+ hits: FindHit[];
76
+ stats: FindStats;
77
+ elapsedMs: number;
78
+ /** Session cwd; hit paths are relative to it. */
79
+ cwd: string;
80
+ /** Display form of the searched directory when narrower than cwd. */
81
+ scopePath?: string;
82
+ meta?: OutputMeta;
83
+ }
84
+
85
+ interface FindRenderArgs {
86
+ query?: string;
87
+ grep_keywords?: string[];
88
+ path?: string;
89
+ }
90
+
91
+ /** Cells in the per-hit score gauge. */
92
+ const GAUGE_WIDTH = 6;
93
+ /** Hits shown before expansion. */
94
+ const COLLAPSED_HITS = 5;
95
+ /** Ranges shown per hit: one collapsed, three expanded. */
96
+ const RANGES_COLLAPSED = 1;
97
+ const RANGES_EXPANDED = 3;
98
+ /** Score at or above which a hit renders as strong / plausible. */
99
+ const STRONG = 0.7;
100
+ const PLAUSIBLE = 0.4;
101
+
102
+ function scoreColor(p: number): ThemeColor {
103
+ return p >= STRONG ? "success" : p >= PLAUSIBLE ? "warning" : "muted";
104
+ }
105
+
106
+ function gauge(p: number, theme: Theme): string {
107
+ const color = scoreColor(p);
108
+ return renderProgressBar(p, GAUGE_WIDTH, {
109
+ style: {
110
+ filled: theme.symbol("progress.filled"),
111
+ empty: theme.symbol("progress.empty"),
112
+ styleFilled: text => theme.fg(color, text),
113
+ styleEmpty: text => theme.fg("dim", text),
114
+ },
115
+ });
116
+ }
117
+
118
+ function renderHit(hit: FindHit, rangeLimit: number, cwd: string | undefined, theme: Theme): string[] {
119
+ const absPath = cwd === undefined ? undefined : path.join(cwd, hit.rel);
120
+ const link = (text: string, line?: number) =>
121
+ absPath === undefined ? text : fileHyperlink(absPath, text, { line });
122
+ const coverage = hit.truncated ? `${hit.linesSeen} lines judged, partial` : `${hit.linesSeen} lines judged`;
123
+ const lines = [
124
+ `${gauge(hit.contentScore, theme)} ${theme.fg(scoreColor(hit.contentScore), hit.contentScore.toFixed(2))} ${link(theme.fg("accent", hit.rel))} ${theme.fg("dim", coverage)}`,
125
+ ];
126
+ const ranges = [...hit.ranges].sort((a, b) => b.p - a.p || a.start - b.start).slice(0, rangeLimit);
127
+ ranges.forEach((range, index) => {
128
+ const span = range.start === range.end ? String(range.start) : `${range.start}-${range.end}`;
129
+ const ref = link(theme.fg("muted", `:${span}`), range.start);
130
+ const snippet = theme.fg("toolOutput", replaceTabs(range.snippet.trim()));
131
+ const branch = theme.fg("dim", getTreeBranch(index === ranges.length - 1, theme));
132
+ lines.push(` ${branch} ${ref} ${theme.fg("dim", range.p.toFixed(2))} ${snippet}`);
133
+ });
134
+ return lines;
135
+ }
136
+
137
+ function quoteQuery(query: string | undefined): string | undefined {
138
+ return query === undefined ? undefined : `"${query}"`;
139
+ }
140
+
141
+ /** Render find calls and results in the transcript. */
142
+ export const findToolRenderer = {
143
+ renderCall(args: FindRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
144
+ const keywords = args.grep_keywords ?? [];
145
+ const meta = keywords.length > 0 ? [keywords.join(" ")] : [];
146
+ if (args.path) meta.push(`in ${args.path}`);
147
+ const text = renderStatusLine(
148
+ {
149
+ icon: "pending",
150
+ spinnerFrame: options.spinnerFrame,
151
+ title: "Find",
152
+ titleColor: "toolTitle",
153
+ description: quoteQuery(args.query),
154
+ meta,
155
+ },
156
+ uiTheme,
157
+ );
158
+ return new Text(text, 1, 0);
159
+ },
160
+
161
+ renderResult(
162
+ result: { content: Array<{ type: string; text?: string }>; details?: FindToolDetails; isError?: boolean },
163
+ options: RenderResultOptions,
164
+ uiTheme: Theme,
165
+ args?: FindRenderArgs,
166
+ ): Component {
167
+ const details = result.details;
168
+ const text = result.content?.find(c => c.type === "text")?.text ?? "";
169
+
170
+ if (result.isError) {
171
+ return new Text(formatErrorMessage(text || "Unknown error", uiTheme), 1, 0);
172
+ }
173
+
174
+ // Streaming progress: the tool reports the phase it is in.
175
+ if (options.isPartial || details === undefined) {
176
+ const header = renderStatusLine(
177
+ {
178
+ icon: "pending",
179
+ spinnerFrame: options.spinnerFrame,
180
+ title: "Find",
181
+ titleColor: "toolTitle",
182
+ description: quoteQuery(args?.query ?? details?.query),
183
+ meta: text ? [text] : [],
184
+ },
185
+ uiTheme,
186
+ );
187
+ return new Text(header, 1, 0);
188
+ }
189
+
190
+ const { hits, stats, threshold } = details;
191
+ const description = quoteQuery(details.query);
192
+ const scope = details.scopePath === undefined ? [] : [`in ${details.scopePath}`];
193
+ const meta = [
194
+ formatCount("hit", hits.length),
195
+ ...scope,
196
+ `${stats.filesRead} files read`,
197
+ `τ ${threshold.toFixed(2)}`,
198
+ `${formatNumber(stats.inputTokens)} tokens`,
199
+ `$${stats.cost.toFixed(4)}`,
200
+ formatDuration(details.elapsedMs),
201
+ ];
202
+ if (stats.errors > 0) meta.push(uiTheme.fg("warning", `${stats.errors} failed`));
203
+
204
+ if (hits.length === 0) {
205
+ const emptyMeta = ["0 hits", ...scope, `$${stats.cost.toFixed(4)}`, formatDuration(details.elapsedMs)];
206
+ if (stats.errors > 0) emptyMeta.push(uiTheme.fg("warning", `${stats.errors} failed`));
207
+ const header = renderStatusLine(
208
+ { icon: "warning", title: "Find", titleColor: "toolTitle", description, meta: emptyMeta },
209
+ uiTheme,
210
+ );
211
+ const lines = [header, formatEmptyMessage("No relevant passages found", uiTheme)];
212
+ for (const failure of stats.failures) lines.push(uiTheme.fg("warning", failure));
213
+ return new Text(lines.join("\n"), 1, 0);
214
+ }
215
+
216
+ const header = renderStatusLine(
217
+ {
218
+ iconOverride: uiTheme.fg("toolTitle", uiTheme.symbol("icon.search")),
219
+ title: "Find",
220
+ titleColor: "toolTitle",
221
+ description,
222
+ meta,
223
+ },
224
+ uiTheme,
225
+ );
226
+
227
+ return createCachedComponent(
228
+ () => options.expanded,
229
+ (width, expanded) => {
230
+ const hitLines = renderTreeList(
231
+ {
232
+ items: hits,
233
+ expanded,
234
+ maxCollapsed: COLLAPSED_HITS,
235
+ maxCollapsedLines: PREVIEW_LIMITS.EXPANDED_LINES,
236
+ itemType: "hit",
237
+ renderItem: hit =>
238
+ renderHit(hit, expanded ? RANGES_EXPANDED : RANGES_COLLAPSED, details.cwd, uiTheme),
239
+ },
240
+ uiTheme,
241
+ );
242
+ const extra: string[] = [];
243
+ if (expanded) {
244
+ extra.push(uiTheme.fg("dim", `keywords: ${details.keywords.join(", ")}`));
245
+ for (const failure of stats.failures) extra.push(uiTheme.fg("warning", failure));
246
+ }
247
+ return [header, ...hitLines, ...extra].map(line => truncateToWidth(line, width, Ellipsis.Omit));
248
+ },
249
+ { paddingX: 1 },
250
+ );
251
+ },
252
+ mergeCallAndResult: true,
253
+ animatedPendingPreview: true,
254
+ animatedPartialResult: true,
255
+ } satisfies ToolRenderer<FindRenderArgs, FindToolDetails>;
package/src/tools/hub.ts CHANGED
@@ -870,6 +870,8 @@ export function launchRenderResult(
870
870
  : "Wait timed out.",
871
871
  ),
872
872
  );
873
+ } else if (details && params.pattern && details.matched === undefined) {
874
+ body.push(theme.fg("warning", `Process exited before output pattern /${params.pattern}/ matched.`));
873
875
  }
874
876
  break;
875
877
  }
@@ -10,6 +10,7 @@ import { bashToolRenderer } from "./bash";
10
10
  import { debugToolRenderer } from "./debug";
11
11
  import { editToolRenderer } from "./edit";
12
12
  import { evalToolRenderer } from "./eval";
13
+ import { findToolRenderer } from "./find";
13
14
  import { githubToolRenderer } from "./github";
14
15
  import { globToolRenderer } from "./glob";
15
16
  import { goalToolRenderer } from "./goal";
@@ -40,6 +41,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
40
41
  eval: evalToolRenderer,
41
42
  edit: editToolRenderer,
42
43
  apply_patch: editToolRenderer,
44
+ find: findToolRenderer,
43
45
  glob: globToolRenderer,
44
46
  grep: grepToolRenderer,
45
47
  lsp: lspToolRenderer,
@@ -290,6 +290,11 @@ export const SEARCH_PROVIDER_OPTIONS = [
290
290
  description:
291
291
  "Grok web search via xAI Responses API (uses SuperGrok/X Premium+ OAuth via /login xai-oauth, or XAI_API_KEY)",
292
292
  },
293
+ {
294
+ value: "openrouter",
295
+ label: "OpenRouter",
296
+ description: "OpenRouter plugins-based web search with model-selected grounding",
297
+ },
293
298
  { value: "zai", label: "Z.AI", description: "Calls Z.AI webSearchPrime MCP" },
294
299
  { value: "exa", label: "Exa", description: "API via /login exa or EXA_API_KEY; explicit keyless fallback via MCP" },
295
300
  { value: "tinyfish", label: "TinyFish", description: "Requires TINYFISH_API_KEY" },
package/src/tui.ts CHANGED
@@ -1257,6 +1257,13 @@ export class TUI extends Container {
1257
1257
  if (!supported && isInsideHerdr() && status === 0) return;
1258
1258
  this.#setSynchronizedOutput(supported);
1259
1259
  });
1260
+ // Icons painted before the Glyph Protocol registration landed may sit in
1261
+ // the terminal as tofu; a full repaint re-emits them against the glossary.
1262
+ this.terminal.onGlyphProtocolReport?.(supported => {
1263
+ if (!supported || this.#stopped) return;
1264
+ this.invalidate();
1265
+ this.requestRender(true);
1266
+ });
1260
1267
  this.terminal.start(
1261
1268
  data => this.#handleInput(data),
1262
1269
  () => {
@@ -1,9 +0,0 @@
1
- /**
2
- * Build a case-sensitive magic-keyword matcher for prose punctuation boundaries.
3
- *
4
- * Sentence punctuation and quotes may touch the keyword, but letters, digits,
5
- * underscores, slashes, backslashes, hyphens, file-extension dots, symbol
6
- * references (`foo::keyword`), and immediate call parentheses (`keyword()`)
7
- * keep the occurrence embedded in code rather than prose.
8
- */
9
- export declare function magicKeywordRegex(keyword: string, flags?: string): RegExp;
@@ -1,13 +0,0 @@
1
- import { type KeywordHighlighter } from "./gradient-highlight.js";
2
- /**
3
- * Whether `text` contains the standalone keyword "orchestrate" (lowercase,
4
- * prose-delimited) in prose — never inside a code block, inline code span,
5
- * or XML/HTML section.
6
- */
7
- export declare function containsOrchestrate(text: string): boolean;
8
- /**
9
- * Highlight every standalone "orchestrate" in `text` for editor display with a
10
- * cool teal→violet gradient (hue 150..280), visually distinct from ultrathink's
11
- * full-spectrum rainbow.
12
- */
13
- export declare const highlightOrchestrate: KeywordHighlighter;
@@ -1,13 +0,0 @@
1
- import { type KeywordHighlighter } from "./gradient-highlight.js";
2
- /**
3
- * Whether `text` contains the standalone keyword "ultrathink" (lowercase,
4
- * prose-delimited) in prose — never inside a code block, inline code span,
5
- * or XML/HTML section.
6
- */
7
- export declare function containsUltrathink(text: string): boolean;
8
- /**
9
- * Rainbow-highlight every standalone "ultrathink" in `text` for editor display.
10
- * Sweeps red→violet (hue 0..330), stopping short of the wrap back to red so the
11
- * gradient resolves smoothly regardless of casing or match length.
12
- */
13
- export declare const highlightUltrathink: KeywordHighlighter;
@@ -1,13 +0,0 @@
1
- import { type KeywordHighlighter } from "./gradient-highlight.js";
2
- /**
3
- * Whether `text` contains the standalone keyword "workflowz"
4
- * (lowercase, prose-delimited) in prose — never inside a code block, inline
5
- * code span, or XML/HTML section.
6
- */
7
- export declare function containsWorkflow(text: string): boolean;
8
- /**
9
- * Highlight every standalone "workflowz" in `text` for editor display
10
- * with a warm amber→green gradient (hue 30..150), visually distinct from
11
- * ultrathink's rainbow and orchestrate's teal→violet.
12
- */
13
- export declare const highlightWorkflow: KeywordHighlighter;
@@ -1,23 +0,0 @@
1
- /** Characters that bind a magic keyword into an identifier or path segment. */
2
- const LEFT_BOUNDARY = String.raw`(?<![\p{L}\p{N}_./\\-])(?<!::)`;
3
-
4
- /** Characters that cannot immediately follow a standalone magic keyword. */
5
- const RIGHT_BOUNDARY = String.raw`(?![\p{L}\p{N}_/\\-])(?!\.[\p{L}\p{N}_-])(?!\()`;
6
-
7
- /** Escape a literal string for safe insertion into a RegExp source. */
8
- function escapeRegExp(value: string): string {
9
- return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
10
- }
11
-
12
- /**
13
- * Build a case-sensitive magic-keyword matcher for prose punctuation boundaries.
14
- *
15
- * Sentence punctuation and quotes may touch the keyword, but letters, digits,
16
- * underscores, slashes, backslashes, hyphens, file-extension dots, symbol
17
- * references (`foo::keyword`), and immediate call parentheses (`keyword()`)
18
- * keep the occurrence embedded in code rather than prose.
19
- */
20
- export function magicKeywordRegex(keyword: string, flags = ""): RegExp {
21
- const normalizedFlags = flags.includes("u") ? flags : `${flags}u`;
22
- return new RegExp(`${LEFT_BOUNDARY}${escapeRegExp(keyword)}${RIGHT_BOUNDARY}`, normalizedFlags);
23
- }
@@ -1,26 +0,0 @@
1
- import { createGradientHighlighter, type KeywordHighlighter } from "./gradient-highlight";
2
- import { magicKeywordRegex } from "./magic-keyword-boundary";
3
- import { keywordInProse } from "./markdown-prose";
4
-
5
- const ORCHESTRATE_WORD = magicKeywordRegex("orchestrate");
6
-
7
- /**
8
- * Whether `text` contains the standalone keyword "orchestrate" (lowercase,
9
- * prose-delimited) in prose — never inside a code block, inline code span,
10
- * or XML/HTML section.
11
- */
12
- export function containsOrchestrate(text: string): boolean {
13
- return keywordInProse(text, ORCHESTRATE_WORD);
14
- }
15
-
16
- /**
17
- * Highlight every standalone "orchestrate" in `text` for editor display with a
18
- * cool teal→violet gradient (hue 150..280), visually distinct from ultrathink's
19
- * full-spectrum rainbow.
20
- */
21
- export const highlightOrchestrate: KeywordHighlighter = createGradientHighlighter({
22
- probe: /orchestrate/,
23
- highlight: magicKeywordRegex("orchestrate", "g"),
24
- stops: 14,
25
- hue: t => 150 + t * 130,
26
- });
@@ -1,26 +0,0 @@
1
- import { createGradientHighlighter, type KeywordHighlighter } from "./gradient-highlight";
2
- import { magicKeywordRegex } from "./magic-keyword-boundary";
3
- import { keywordInProse } from "./markdown-prose";
4
-
5
- const ULTRATHINK_WORD = magicKeywordRegex("ultrathink");
6
-
7
- /**
8
- * Whether `text` contains the standalone keyword "ultrathink" (lowercase,
9
- * prose-delimited) in prose — never inside a code block, inline code span,
10
- * or XML/HTML section.
11
- */
12
- export function containsUltrathink(text: string): boolean {
13
- return keywordInProse(text, ULTRATHINK_WORD);
14
- }
15
-
16
- /**
17
- * Rainbow-highlight every standalone "ultrathink" in `text` for editor display.
18
- * Sweeps red→violet (hue 0..330), stopping short of the wrap back to red so the
19
- * gradient resolves smoothly regardless of casing or match length.
20
- */
21
- export const highlightUltrathink: KeywordHighlighter = createGradientHighlighter({
22
- probe: /ultrathink/,
23
- highlight: magicKeywordRegex("ultrathink", "g"),
24
- stops: 14,
25
- hue: t => t * 330,
26
- });
@@ -1,26 +0,0 @@
1
- import { createGradientHighlighter, type KeywordHighlighter } from "./gradient-highlight";
2
- import { magicKeywordRegex } from "./magic-keyword-boundary";
3
- import { keywordInProse } from "./markdown-prose";
4
-
5
- const WORKFLOW_WORD = magicKeywordRegex("workflowz");
6
-
7
- /**
8
- * Whether `text` contains the standalone keyword "workflowz"
9
- * (lowercase, prose-delimited) in prose — never inside a code block, inline
10
- * code span, or XML/HTML section.
11
- */
12
- export function containsWorkflow(text: string): boolean {
13
- return keywordInProse(text, WORKFLOW_WORD);
14
- }
15
-
16
- /**
17
- * Highlight every standalone "workflowz" in `text` for editor display
18
- * with a warm amber→green gradient (hue 30..150), visually distinct from
19
- * ultrathink's rainbow and orchestrate's teal→violet.
20
- */
21
- export const highlightWorkflow: KeywordHighlighter = createGradientHighlighter({
22
- probe: /workflowz/,
23
- highlight: magicKeywordRegex("workflowz", "g"),
24
- stops: 14,
25
- hue: t => 30 + t * 120,
26
- });