feat(web): render grep/glob search output as a search card
Consume the card:'search' result view (matches grouped by file for grep, a path list for glob) the search backend PR added. SearchBlock (ui-primitives) draws both kinds via the kind discriminant with a per-file collapse, a truncation pill, a height cap matching TerminalBlock, and a copy control; search-card-model is the single resultView derivation; a keyed SearchRow registers under grep and glob with the card resident under its summary. The generic fallback and the details panel are search-aware. Fixture gains grep and glob turns for the built-boot snapshot.
This commit is contained in:
@@ -19,6 +19,7 @@ import { InputBar } from './skeleton/InputBar.tsx'
|
||||
import { ChatView } from './chat/ChatView.tsx'
|
||||
import { StatsLine } from './chat/StatsLine.tsx'
|
||||
import { bashToolviewSample } from './toolviews/bash-sample.tsx'
|
||||
import { searchToolview } from './toolviews/search-sample.tsx'
|
||||
import { ApprovalPanel } from './skeleton/ApprovalPanel.tsx'
|
||||
import { todoToolview } from './toolviews/todo-row.tsx'
|
||||
import { todoDockEntry } from './skeleton/TodoPanel.tsx'
|
||||
@@ -254,6 +255,10 @@ export function apply(ctx: Context): void {
|
||||
// (ToolRow-matching Bash · {description} chrome; scoped badge in child sessions).
|
||||
ctx.plugin(bashToolviewSample)
|
||||
|
||||
// The grep/glob search row rides the same seam: one component registered
|
||||
// under both tool names, since both declare the same search render intent.
|
||||
ctx.plugin(searchToolview)
|
||||
|
||||
// The todo_write row rides the same seam (a product registration, not a sample).
|
||||
ctx.plugin(todoToolview)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
IconThinkOutline14,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { ToolRowOwnerProps } from '../contract/slots.ts'
|
||||
import { searchCardModel } from '../contract/search-card-model.ts'
|
||||
import { terminalCardModel } from '../contract/terminal-card-model.ts'
|
||||
import { toolRowModel, type ToolRowVariant } from '../contract/tool-call-model.ts'
|
||||
import { ToolRow } from './ToolRow.tsx'
|
||||
@@ -29,6 +30,7 @@ const VARIANT_ICONS: Record<ToolRowVariant, ReactNode> = {
|
||||
export function GenericToolCard({ toolName, block, cwd, openFile }: ToolRowOwnerProps) {
|
||||
const model = toolRowModel(toolName, block, cwd)
|
||||
const terminal = terminalCardModel(block, cwd)
|
||||
const search = searchCardModel(block)
|
||||
const singleFile = model.filePath !== undefined
|
||||
return (
|
||||
<ToolRow
|
||||
@@ -37,11 +39,13 @@ export function GenericToolCard({ toolName, block, cwd, openFile }: ToolRowOwner
|
||||
icon={VARIANT_ICONS[model.variant]}
|
||||
title={model.title}
|
||||
// A terminal presenter's description is the contract's above-card text, so
|
||||
// it outranks the args-derived summary here exactly as it does in BashRow.
|
||||
summary={terminal?.description ?? model.summary}
|
||||
// it outranks the args-derived summary here exactly as it does in BashRow;
|
||||
// a search result view's replacement title outranks it the same way.
|
||||
summary={terminal?.description ?? search?.title ?? model.summary}
|
||||
// Single-file tools never expose an args body — the path link is the only action.
|
||||
body={singleFile ? null : model.body}
|
||||
terminal={terminal}
|
||||
search={search}
|
||||
state={model.state}
|
||||
filePath={model.filePath}
|
||||
onOpenFile={singleFile ? openFile : undefined}
|
||||
|
||||
@@ -175,14 +175,15 @@ button.leading {
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
/* The two block-shaped expanded bodies: the code variant's run_code program
|
||||
through CodeBlock (shiki-highlighted TypeScript) and a terminal card's
|
||||
command output through TerminalBlock. Both are drawn by the shared
|
||||
primitive, so only the row's indentation is this file's concern — the margin
|
||||
also replaces each primitive's own standalone vertical spacing with the
|
||||
flow's row rhythm. */
|
||||
/* The block-shaped expanded bodies: the code variant's run_code program
|
||||
through CodeBlock (shiki-highlighted TypeScript), a terminal card's command
|
||||
output through TerminalBlock, and a search card's grouped matches or path
|
||||
list through SearchBlock. All are drawn by the shared primitive, so only the
|
||||
row's indentation is this file's concern — the margin also replaces each
|
||||
primitive's own standalone vertical spacing with the flow's row rhythm. */
|
||||
.codeBody,
|
||||
.terminalBody {
|
||||
.terminalBody,
|
||||
.searchBody {
|
||||
margin: 4px 0 4px 22px;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
// 16px leading slot (state dot / tool icon, chevron on hover or expanded) + title +
|
||||
// separator dot + FILL-truncated summary. The collapsed row is always one
|
||||
// line; the expanded body is indented gray text, the run_code program through
|
||||
// CodeBlock, or — for a call whose render intent is a terminal card — the
|
||||
// command's own output through TerminalBlock, capped at
|
||||
// CHAT_TERMINAL_MAX_LINES so the message flow stays scannable. Expand state is
|
||||
// component-local view state. File-tool summaries are path links that open
|
||||
// through the host; the row itself is not a details-panel control.
|
||||
// CodeBlock, a call whose render intent is a terminal card through TerminalBlock
|
||||
// (capped at CHAT_TERMINAL_MAX_LINES), or a search card through SearchBlock
|
||||
// (capped at CHAT_SEARCH_MAX_LINES), so the message flow stays scannable. Expand
|
||||
// state is component-local view state. File-tool summaries are path links that
|
||||
// open through the host; the row itself is not a details-panel control.
|
||||
|
||||
import { useState, type KeyboardEvent, type MouseEvent, type ReactNode } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { CodeBlock, StateDot, TerminalBlock } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { CodeBlock, SearchBlock, StateDot, TerminalBlock } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { CHAT_SEARCH_MAX_LINES, type SearchCardModel } from '../contract/search-card-model.ts'
|
||||
import { CHAT_TERMINAL_MAX_LINES, type TerminalCardModel } from '../contract/terminal-card-model.ts'
|
||||
import type { ToolRowState, ToolRowVariant } from '../contract/tool-call-model.ts'
|
||||
import css from './ToolRow.module.css'
|
||||
@@ -33,6 +34,13 @@ export interface ToolRowProps {
|
||||
* expandable (its leading slot never toggles).
|
||||
*/
|
||||
terminal?: TerminalCardModel | null | undefined
|
||||
/**
|
||||
* Search-card material for a call whose render intent is a search card
|
||||
* (derived by `searchCardModel`); it replaces the text body when present.
|
||||
* Null or absent leaves the text body. A call carries at most one card kind,
|
||||
* so `terminal` and `search` are never both present on the same row.
|
||||
*/
|
||||
search?: SearchCardModel | null | undefined
|
||||
state: ToolRowState
|
||||
/** Makes the row itself the expand control instead of only its leading icon. */
|
||||
expandOnRowClick?: boolean | undefined
|
||||
@@ -64,6 +72,7 @@ export function ToolRow({
|
||||
summary,
|
||||
body,
|
||||
terminal,
|
||||
search,
|
||||
state,
|
||||
expandOnRowClick = false,
|
||||
filePath,
|
||||
@@ -71,16 +80,17 @@ export function ToolRow({
|
||||
}: ToolRowProps) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const terminalBody = terminal ?? null
|
||||
const searchBody = search ?? null
|
||||
// A row that names a single file keeps one interaction (open that path);
|
||||
// args expand is off whether or not the open callback is wired yet. Terminal
|
||||
// args expand is off whether or not the open callback is wired yet. Card
|
||||
// material still expands: only the file variants carry a path, so a terminal
|
||||
// card and a file link never land on the same row.
|
||||
// or search card and a file link never land on the same row.
|
||||
const singleFile = filePath !== undefined
|
||||
const fileLink = singleFile && onOpenFile !== undefined
|
||||
const expandable = (body !== null && !singleFile) || terminalBody !== null
|
||||
const expandable = (body !== null && !singleFile) || terminalBody !== null || searchBody !== null
|
||||
// The text arms take the empty string for a null body: a row expandable
|
||||
// only through its terminal material renders the terminal body instead, so
|
||||
// this substitution never shows.
|
||||
// only through its card material renders that card instead, so this
|
||||
// substitution never shows.
|
||||
const text = body ?? ''
|
||||
const open = expanded && expandable
|
||||
const rowExpands = expandable && expandOnRowClick
|
||||
@@ -164,9 +174,11 @@ export function ToolRow({
|
||||
)}
|
||||
{open && (terminalBody !== null
|
||||
? <TerminalBlock {...terminalBody.card} maxLines={CHAT_TERMINAL_MAX_LINES} className={css.terminalBody} />
|
||||
: variant === 'code'
|
||||
? <CodeBlock code={text} lang="typescript" className={css.codeBody} />
|
||||
: <div className={css.body}>{text}</div>)}
|
||||
: searchBody !== null
|
||||
? <SearchBlock {...searchBody.card} maxLines={CHAT_SEARCH_MAX_LINES} className={css.searchBody} />
|
||||
: variant === 'code'
|
||||
? <CodeBlock code={text} lang="typescript" className={css.codeBody} />
|
||||
: <div className={css.body}>{text}</div>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Pure derivation of the search-card props from a frozen call slice: the
|
||||
* `card:'search'` render intent the `grep` and `glob` tools declare arrives on
|
||||
* the snapshot as `resultView`, and this is the one place that turns it into
|
||||
* what {@link SearchBlock} draws. Both conversation render sites (the chat tool
|
||||
* row's resident body and the details panel's Output section) call this, so the
|
||||
* grouped matches or the path list they show are derived once.
|
||||
*
|
||||
* The search card is result-time only: a search call has no matches or paths
|
||||
* before `execute`, so its pending state stays a `GenericCallView`
|
||||
* ({@link module:@deepseek-ai/dsh-tools/src/presentation}). This derivation
|
||||
* therefore reads only `resultView` and returns null for a still-running call,
|
||||
* unlike the terminal card whose call view carries the command before
|
||||
* execution.
|
||||
* @module
|
||||
*/
|
||||
import type { SearchBlockProps } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { ToolCallBlock } from './tool-call-model.ts'
|
||||
|
||||
/**
|
||||
* Distributive `Omit`: a plain `Omit<A | B, K>` keeps only the keys common to
|
||||
* both members, which would drop the `files`/`paths` discriminated fields.
|
||||
* Distributing over the naked type parameter `T` preserves each shape.
|
||||
*/
|
||||
type DistributiveOmit<T, K extends keyof T> = T extends unknown ? Omit<T, K> : never
|
||||
|
||||
/** The {@link SearchBlockProps} union minus each render site's own fields. */
|
||||
type SearchBlockModelProps = DistributiveOmit<SearchBlockProps, 'maxLines' | 'className'>
|
||||
|
||||
/**
|
||||
* Result rows the chat row's resident search body shows before collapsing the
|
||||
* middle — half the primitive's own default, which the details panel keeps. A
|
||||
* chat row is a summary surface inside the message flow: the flow must stay
|
||||
* scannable across many calls, while the details panel is the single-call
|
||||
* reading surface. A design constant of this UI's row geometry, not a
|
||||
* deployment choice, so it is fixed here rather than a plugin Config field.
|
||||
*/
|
||||
export const CHAT_SEARCH_MAX_LINES = 8
|
||||
|
||||
/**
|
||||
* The {@link SearchBlock} props this derivation owns. Held as a nested object
|
||||
* (`card`) so a render site spreads exactly the primitive's own surface and can
|
||||
* never leak a neighbouring field into it. `maxLines`/`className` belong to each
|
||||
* render site.
|
||||
*/
|
||||
export interface SearchCardModel {
|
||||
/**
|
||||
* The props {@link SearchBlock} draws, minus each render site's own
|
||||
* `maxLines`/`className`.
|
||||
*/
|
||||
card: SearchBlockModelProps
|
||||
/**
|
||||
* The result view's replacement title, which the presentation contract lets a
|
||||
* search tool set at settle time. Absent when the presenter supplied none; a
|
||||
* row then keeps its args-derived summary.
|
||||
*/
|
||||
title: string | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the search-card props for a tool call, or null when this call is not a
|
||||
* search card and belongs on the generic path.
|
||||
*
|
||||
* Only the result side matters: the search card carries no call-time state, so
|
||||
* a still-running call (no result view) is null, as is a settled call whose
|
||||
* result view is not a search card — including a `card` value this UI version
|
||||
* does not know, which arrives over the wire and cannot be trusted to be one of
|
||||
* the compiled variants, and a generic result a `grep`/`glob` failure or nested
|
||||
* `run_code` dispatch produces (its text keeps the generic path).
|
||||
* @param block - RunningToolCall or ToolResultNode off the snapshot caches.
|
||||
* @returns the search-card props, or null for the generic path.
|
||||
*/
|
||||
export function searchCardModel(block: ToolCallBlock): SearchCardModel | null {
|
||||
// Running: no result view exists yet, and a search card is result-only.
|
||||
if (!('kind' in block)) return null
|
||||
const result = block.resultView?.card === 'search' ? block.resultView : null
|
||||
if (result === null) return null
|
||||
const common = { truncated: result.truncated, total: result.total }
|
||||
return {
|
||||
title: result.title,
|
||||
card: result.kind === 'matches'
|
||||
? { kind: 'matches', files: result.files, ...common }
|
||||
: { kind: 'paths', paths: result.paths, ...common },
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,11 @@
|
||||
// share the store seat exists for) and derives the call material from the
|
||||
// session snapshot — no data of its own.
|
||||
|
||||
import { CodeBlock, TerminalBlock } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { CodeBlock, SearchBlock, TerminalBlock } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { shallowEqual } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ConversationSnapshot, RunningToolCall, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { DetailsSlotProps } from '../contract/slots.ts'
|
||||
import { searchCardModel } from '../contract/search-card-model.ts'
|
||||
import { terminalCardModel } from '../contract/terminal-card-model.ts'
|
||||
import type { ToolCallBlock } from '../contract/tool-call-model.ts'
|
||||
import css from './DetailsPanel.module.css'
|
||||
@@ -127,8 +128,10 @@ export function DetailsPanel({ useSession, useSessions, sessionId, useStore, clo
|
||||
* The Output section's body for the selected call. A terminal-card call — a
|
||||
* shell command's call/result views — renders through the shared TerminalBlock
|
||||
* at the primitive's own full height allowance, so column-aligned output keeps
|
||||
* its alignment and scrolls sideways instead of folding. Every other call, and
|
||||
* a running call with no terminal card yet, keeps the flattened text form.
|
||||
* its alignment and scrolls sideways instead of folding. A search-card call —
|
||||
* a `grep`/`glob` result view — renders through the shared SearchBlock at the
|
||||
* same full height allowance. Every other call, and a running call with no card
|
||||
* yet, keeps the flattened text form.
|
||||
* @param props.material - the selected call's material from {@link materialFor}.
|
||||
* @param props.cwd - the session workspace root, resolving the terminal view's cwd.
|
||||
* @returns the Output section's body element.
|
||||
@@ -147,6 +150,8 @@ function OutputBody({ material, cwd }: { material: CallMaterial; cwd: string | u
|
||||
</>
|
||||
)
|
||||
}
|
||||
const search = searchCardModel(material.block)
|
||||
if (search !== null) return <SearchBlock {...search.card} className={css.terminal} />
|
||||
// A settled call always carries the result node the flattened form needs;
|
||||
// the running shape has no result to flatten.
|
||||
if (!('kind' in material.block)) return <div className={css.empty}>运行中…</div>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/* Search toolview: same geometry/tokens as ToolRow and BashRow (figma
|
||||
Search · summary), plus the search card the row stacks resident under its
|
||||
summary line. */
|
||||
|
||||
/* Summary line over the search card; the summary row keeps its own 24px
|
||||
height, so the card is a column around it rather than a change to it. */
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Row indentation matches ToolRow's expanded bodies (16px leading + 6px gap),
|
||||
and replaces the primitive's standalone vertical margin with the flow's. */
|
||||
.search {
|
||||
margin: 4px 0 4px 22px;
|
||||
}
|
||||
|
||||
.root {
|
||||
position: relative; /* sweep-glare overlay anchor */
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Running sweep glare — same deepsuite ShimmerText pattern as ToolRow / BashRow. */
|
||||
.root[data-state='running']::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 300px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%,
|
||||
transparent 100%
|
||||
);
|
||||
animation: dsh-search-row-sweep 2.6s ease-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes dsh-search-row-sweep {
|
||||
0% { left: -300px; }
|
||||
90%, 100% { left: 100%; }
|
||||
}
|
||||
|
||||
.leading {
|
||||
flex: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 6px;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: none;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
.sep {
|
||||
flex: none;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
border-radius: 1px;
|
||||
margin: 0 8px;
|
||||
background: var(--dsw-alias-label-caption);
|
||||
}
|
||||
|
||||
.summary {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.visuallyHidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Search toolview registrant: the keyed toolview hole (ctx.slots.register +
|
||||
// ToolRowProps only — never imports the chat domain). One SearchRow component
|
||||
// registered under both `grep` and `glob`, since both tools declare the same
|
||||
// `card: 'search'` render intent and render as one visual object; the row reads
|
||||
// the `kind` discriminant off the derived model to draw grouped matches or a
|
||||
// path list. Product chrome matches ToolRow / BashRow (Search · {summary}).
|
||||
//
|
||||
// A search call declares its render intent result-time only, so this row's
|
||||
// search card is resident below the summary rather than expand-gated: the row
|
||||
// itself has no expand control, and the card's own copy, per-file collapse, and
|
||||
// head/tail expand are the row's only interactions. CHAT_SEARCH_MAX_LINES is
|
||||
// passed as `maxLines` — the chat flow's tighter cap over the block's own
|
||||
// default of 16 — so a large result stays bounded in the message flow.
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import { IconSearchOutline16, SearchBlock, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { ToolRowProps } from '../contract/slots.ts'
|
||||
import { CHAT_SEARCH_MAX_LINES, searchCardModel } from '../contract/search-card-model.ts'
|
||||
import { toolRowModel, type ToolRowState } from '../contract/tool-call-model.ts'
|
||||
import css from './search-sample.module.css'
|
||||
|
||||
/** Leading-slot glyph substitution: the search icon yields to the terminal
|
||||
* state semantic (error = red, interrupted = amber). Running keeps the icon —
|
||||
* the row sweep carries the in-flight signal. */
|
||||
function leadingFor(state: ToolRowState) {
|
||||
switch (state) {
|
||||
case 'error': return <StateDot state="error" />
|
||||
case 'stopped': return <StateDot state="warning" />
|
||||
default: return <IconSearchOutline16 size={14} />
|
||||
}
|
||||
}
|
||||
|
||||
/** Visually hidden status — StateDot is aria-hidden; assistive technology needs a text label. */
|
||||
function stateStatus(state: ToolRowState): string | null {
|
||||
switch (state) {
|
||||
case 'running': return '运行中'
|
||||
case 'error': return '失败'
|
||||
case 'stopped': return '已停止'
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search row: icon + Search · {summary} in the shared ToolRow chrome, with the
|
||||
* completed search's card resident below it. The summary row is not a
|
||||
* details-panel control, so the card's copy, per-file collapse, and expand
|
||||
* controls are the row's only interactions. Registered under both `grep` and
|
||||
* `glob`; the derived model's `kind` decides the card shape.
|
||||
*/
|
||||
export function SearchRow({ toolName, block }: ToolRowProps) {
|
||||
const model = toolRowModel(toolName, block)
|
||||
const search = searchCardModel(block)
|
||||
const status = stateStatus(model.state)
|
||||
return (
|
||||
<div className={css.card}>
|
||||
<div className={css.root} data-variant="search" data-tool={toolName} data-state={model.state}>
|
||||
<span className={css.leading}>{leadingFor(model.state)}</span>
|
||||
{status !== null && <span className={css.visuallyHidden}>{status}</span>}
|
||||
<span className={css.title}>{model.title}</span>
|
||||
<span className={css.sep} aria-hidden />
|
||||
{/* The result view's replacement title outranks the args-derived
|
||||
summary, matching the terminal card's description precedence. */}
|
||||
<span className={css.summary}>{search?.title ?? model.summary}</span>
|
||||
</div>
|
||||
{search !== null && (
|
||||
<SearchBlock {...search.card} maxLines={CHAT_SEARCH_MAX_LINES} className={css.search} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The search toolview as a plain registrant plugin. `inject` carries the
|
||||
* load-order seam: requiring the conversation service guarantees the chat entry
|
||||
* (and with it the 'conversation.chat.toolview' declaration) is registered.
|
||||
* The one component registers under both keys, since `grep` and `glob` are the
|
||||
* same visual object discriminated only by the result view's `kind`.
|
||||
*/
|
||||
export const searchToolview = {
|
||||
name: 'search-toolview',
|
||||
inject: ['slots', 'conversation'],
|
||||
/**
|
||||
* Register the search row into the chat view's keyed toolview hole under both
|
||||
* the `grep` and `glob` tool names.
|
||||
* @param ctx - registrant context (disposal rides ctx.effect inside slots.register).
|
||||
*/
|
||||
apply(ctx: Context): void {
|
||||
ctx.slots.register({ name: 'conversation.chat.toolview', key: 'grep' }, SearchRow)
|
||||
ctx.slots.register({ name: 'conversation.chat.toolview', key: 'glob' }, SearchRow)
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user