@huanlin/dsh-plugin-merge-tool-calls 0.1.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.
- package/README.md +77 -0
- package/cordis.patch.yml +11 -0
- package/lib/client.js +1029 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +816 -0
- package/lib/invariant.js +11 -0
- package/lib/types/client/card-model.d.ts +77 -0
- package/lib/types/client/index.d.ts +29 -0
- package/lib/types/client/locales.d.ts +47 -0
- package/lib/types/client/merge-run.d.ts +46 -0
- package/lib/types/client/rows.d.ts +46 -0
- package/lib/types/client/styles.d.ts +19 -0
- package/lib/types/client/tool-names.d.ts +30 -0
- package/lib/types/index.d.ts +24 -0
- package/lib/types/invariant.d.ts +12 -0
- package/lib/types/types.d.ts +19 -0
- package/package.json +114 -0
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/invariant.ts
|
|
2
|
+
const PACKAGE_NAME = "@huanlin/dsh-plugin-merge-tool-calls";
|
|
3
|
+
const name = "merge-tool-calls-invariant";
|
|
4
|
+
const inject = ["invariants"];
|
|
5
|
+
/** No runtime invariant: this package contributes client-only slot entries. */
|
|
6
|
+
const install = () => {};
|
|
7
|
+
/** Register package ownership with the invariant service. */
|
|
8
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
9
|
+
|
|
10
|
+
//#endregion
|
|
11
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure card/row derivation from a frozen call slice, mirroring ui-tool's
|
|
3
|
+
* tool-call/read-card/search-card/diff-card/terminal-card/web-card models at
|
|
4
|
+
* the plugin boundary (those models are ui-tool internals and cannot be
|
|
5
|
+
* imported cross-package). Same wire contract, same defensive treatment of
|
|
6
|
+
* untrusted result views. A merged row renders the same surface the built-in
|
|
7
|
+
* row would: a variant title/icon plus a card primitive or IN/OUT text.
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { type ToolCallBlock, type ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client';
|
|
11
|
+
import type { DiffBlockProps, ReadBlockProps, SearchBlockProps, TerminalBlockProps, WebBlockProps } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
12
|
+
import { type ToolVariant } from './tool-names.ts';
|
|
13
|
+
/** Row state semantic, mirroring ui-tool's ToolRowState. */
|
|
14
|
+
export type RowState = 'running' | 'ok' | 'error' | 'stopped';
|
|
15
|
+
export type { ToolVariant } from './tool-names.ts';
|
|
16
|
+
/** Read-card props the ReadBlock primitive draws (per-render maxLines owned by the caller). */
|
|
17
|
+
export type ReadCardModel = Pick<ReadBlockProps, 'label' | 'lines' | 'totalLines' | 'lang'>;
|
|
18
|
+
/**
|
|
19
|
+
* Distributive `Omit`: a plain `Omit<A | B, K>` keeps only the keys common to
|
|
20
|
+
* both members, dropping the discriminated `files`/`paths` fields. Distributing
|
|
21
|
+
* over the naked type parameter preserves each shape.
|
|
22
|
+
*/
|
|
23
|
+
type DistributiveOmit<T, K extends keyof T> = T extends unknown ? Omit<T, K> : never;
|
|
24
|
+
/** Search-card props plus the capped-result recovery locator (mirrors ui-tool). */
|
|
25
|
+
export interface SearchCardModel {
|
|
26
|
+
readonly card: DistributiveOmit<SearchBlockProps, 'maxLines' | 'className'>;
|
|
27
|
+
/** The result view's replacement title, which outranks the args summary. */
|
|
28
|
+
readonly title: string | undefined;
|
|
29
|
+
readonly recovery: string | undefined;
|
|
30
|
+
}
|
|
31
|
+
/** Diff-card props (mirrors ui-tool; maxLines/className belong to the render site). */
|
|
32
|
+
export interface DiffCardModel {
|
|
33
|
+
readonly card: Pick<DiffBlockProps, 'diffs'>;
|
|
34
|
+
}
|
|
35
|
+
/** Terminal-card props (mirrors ui-tool; maxLines/className/labels belong to the render site). */
|
|
36
|
+
export interface TerminalCardModel {
|
|
37
|
+
readonly card: Pick<TerminalBlockProps, 'command' | 'cwd' | 'output' | 'exitCode' | 'signal' | 'running'>;
|
|
38
|
+
/** The call view's model-authored description shown above the card. */
|
|
39
|
+
readonly description: string | undefined;
|
|
40
|
+
}
|
|
41
|
+
/** True when a settled terminal card reports a failing exit (mirrors ui-tool). */
|
|
42
|
+
export declare function terminalFailed(model: TerminalCardModel): boolean;
|
|
43
|
+
/** Everything a merged row needs, derived once from the frozen slice. */
|
|
44
|
+
export interface CallRowModel {
|
|
45
|
+
readonly state: RowState;
|
|
46
|
+
readonly variant: ToolVariant;
|
|
47
|
+
/** Row title: variant title, or the tool-owned title when it refines one. */
|
|
48
|
+
readonly title: string;
|
|
49
|
+
/** Args/result-derived one-line summary (path for file tools, query for searches, …). */
|
|
50
|
+
readonly summary: string;
|
|
51
|
+
/** Expanded-body input text (pretty args); null = no input section. */
|
|
52
|
+
readonly body: string | null;
|
|
53
|
+
/** Flattened result text; null while running or when the result carries no text. */
|
|
54
|
+
readonly output: string | null;
|
|
55
|
+
/** First result line on an error row (the collapsed summary's error text). */
|
|
56
|
+
readonly errorSummary: string | null;
|
|
57
|
+
/** Openable workspace path from args; absent for non-file tools and errors. */
|
|
58
|
+
readonly filePath: string | undefined;
|
|
59
|
+
/** Whether the row has anything to expand (a card, args body, or output). */
|
|
60
|
+
readonly expandable: boolean;
|
|
61
|
+
/** Expanded-body content cards, mutually exclusive, or null when absent. */
|
|
62
|
+
readonly terminal: TerminalCardModel | null;
|
|
63
|
+
readonly diff: DiffCardModel | null;
|
|
64
|
+
readonly read: ReadCardModel | null;
|
|
65
|
+
readonly search: SearchCardModel | null;
|
|
66
|
+
readonly web: WebBlockProps | null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Flatten a settled result's content blocks to display text.
|
|
70
|
+
* @param node - settled result node.
|
|
71
|
+
* @returns joined text (may be empty).
|
|
72
|
+
*/
|
|
73
|
+
export declare function resultText(node: ToolResultNode): string;
|
|
74
|
+
/** Strip the workspace root from a workspace-rooted absolute path (display only). */
|
|
75
|
+
export declare function relativizeToCwd(text: string, cwd: string | undefined): string;
|
|
76
|
+
/** Derive the row model for one call of a grouped tool. */
|
|
77
|
+
export declare function callRowModel(toolName: string, block: ToolCallBlock, cwd: string | undefined): CallRowModel;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* merge-tool-calls — browser half.
|
|
3
|
+
*
|
|
4
|
+
* Shadows the shipped `tool.call.toolview` entries for every grouped tool at
|
|
5
|
+
* priority -1 (the keyed slot's shadowing rule: lowest priority renders). An
|
|
6
|
+
* empty `tools` config means every built-in generic-family tool
|
|
7
|
+
* (see {@link ALL_TOOL_NAMES}); a non-empty list is an explicit whitelist.
|
|
8
|
+
* The shadowed component merges consecutive calls of one tool in the chat flow
|
|
9
|
+
* into a single card with compact child rows.
|
|
10
|
+
*
|
|
11
|
+
* @module @dsh-external/dsh-merge-tool-calls/client
|
|
12
|
+
*/
|
|
13
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
14
|
+
import { type MergeToolCallsConfig } from '../types.ts';
|
|
15
|
+
import { type MergeToolCallsKey } from './locales.ts';
|
|
16
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
17
|
+
interface LocaleNamespaceMap {
|
|
18
|
+
/** Copy for the merged tool-call rows. */
|
|
19
|
+
'merge-tool-calls': MergeToolCallsKey;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Required services: the slot registry (toolview shadowing) and locale. */
|
|
23
|
+
export declare const inject: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Register one shadowed toolview per grouped tool.
|
|
26
|
+
* @param ctx - client root context.
|
|
27
|
+
* @param config - row config; defaults apply when the loader passes none.
|
|
28
|
+
*/
|
|
29
|
+
export declare function apply(ctx: ClientContext, config?: Partial<MergeToolCallsConfig>): void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Product copy for the merged tool-call rows. */
|
|
2
|
+
export declare const zh: {
|
|
3
|
+
running: string;
|
|
4
|
+
failed: string;
|
|
5
|
+
stopped: string;
|
|
6
|
+
expand: string;
|
|
7
|
+
collapse: string;
|
|
8
|
+
more: string;
|
|
9
|
+
showLess: string;
|
|
10
|
+
mergedCount: string;
|
|
11
|
+
'terminal.signal': string;
|
|
12
|
+
'terminal.exitCode': string;
|
|
13
|
+
'terminal.running': string;
|
|
14
|
+
'terminal.failed': string;
|
|
15
|
+
'terminal.done': string;
|
|
16
|
+
'terminal.copy': string;
|
|
17
|
+
'terminal.copied': string;
|
|
18
|
+
'terminal.noOutput': string;
|
|
19
|
+
'terminal.collapseAria': string;
|
|
20
|
+
'terminal.collapse': string;
|
|
21
|
+
'terminal.expandAria': string;
|
|
22
|
+
'terminal.expandRest': string;
|
|
23
|
+
};
|
|
24
|
+
export type MergeToolCallsKey = keyof typeof zh;
|
|
25
|
+
export declare const en: {
|
|
26
|
+
running: string;
|
|
27
|
+
failed: string;
|
|
28
|
+
stopped: string;
|
|
29
|
+
expand: string;
|
|
30
|
+
collapse: string;
|
|
31
|
+
more: string;
|
|
32
|
+
showLess: string;
|
|
33
|
+
mergedCount: string;
|
|
34
|
+
'terminal.signal': string;
|
|
35
|
+
'terminal.exitCode': string;
|
|
36
|
+
'terminal.running': string;
|
|
37
|
+
'terminal.failed': string;
|
|
38
|
+
'terminal.done': string;
|
|
39
|
+
'terminal.copy': string;
|
|
40
|
+
'terminal.copied': string;
|
|
41
|
+
'terminal.noOutput': string;
|
|
42
|
+
'terminal.collapseAria': string;
|
|
43
|
+
'terminal.collapse': string;
|
|
44
|
+
'terminal.expandAria': string;
|
|
45
|
+
'terminal.expandRest': string;
|
|
46
|
+
};
|
|
47
|
+
export declare const NS = "merge-tool-calls";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure detection of a consecutive run of grouped tool calls in the chat flow,
|
|
3
|
+
* and the per-seat merged-group partition over it.
|
|
4
|
+
*
|
|
5
|
+
* A pure function of the chat snapshot (order + node store), so the merged
|
|
6
|
+
* display is deterministic under replay: the web layer recomputes it per frame.
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import type { ChatNodeStore, ToolCallBlock } from '@deepseek-ai/dsh-client-runtime/client';
|
|
10
|
+
import type { MergeGroupMode } from '../types.ts';
|
|
11
|
+
/** The wire tool name of a call in either lifecycle form (mirrors ui-tool). */
|
|
12
|
+
export declare function callNameOf(block: ToolCallBlock): string;
|
|
13
|
+
/** Whether the call's wire name is one of the grouped tools (empty = all). */
|
|
14
|
+
export declare function isGroupedTool(name: string, tools: readonly string[]): boolean;
|
|
15
|
+
/** One merged group: the group's first call plus its consecutive continuations. */
|
|
16
|
+
export interface ReadRun {
|
|
17
|
+
/** Whether this seat is the group's rendering head (the only seat that shows the card). */
|
|
18
|
+
readonly isFirst: boolean;
|
|
19
|
+
/** The group's root blocks in flow order (first + continuations). */
|
|
20
|
+
readonly blocks: readonly ToolCallBlock[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Compute the merged group this call belongs to.
|
|
24
|
+
*
|
|
25
|
+
* 1. Locates this call's node in the chat order; null when it is not a chat
|
|
26
|
+
* tool-call node (e.g. a read dispatched as a subcall).
|
|
27
|
+
* 2. Walks backward/forward to the maximal consecutive run containing it. A
|
|
28
|
+
* call continues the run when it is a grouped tool AND same-tool-same-run:
|
|
29
|
+
* the identical wire name, or a sibling of the same known variant family
|
|
30
|
+
* (grep+glob, bash+pwsh, read+web_fetch…). Unknown names (variant
|
|
31
|
+
* `others`) only merge with themselves, so unrelated tools never share a
|
|
32
|
+
* card (`adjacent`: any consecutive run; `step`: same agent step as this
|
|
33
|
+
* call).
|
|
34
|
+
* 3. Partitions the run into `maxGroupSize`-sized groups; this call is the
|
|
35
|
+
* group's first only when it heads one of those partitions. Truncation
|
|
36
|
+
* therefore never orphans a call: the excess starts its own group.
|
|
37
|
+
*
|
|
38
|
+
* @param order - chat node key order.
|
|
39
|
+
* @param nodes - chat node store.
|
|
40
|
+
* @param myCallId - the call id of the seat asking about itself.
|
|
41
|
+
* @param tools - grouped wire tool names; empty means every tool.
|
|
42
|
+
* @param groupBy - grouping mode.
|
|
43
|
+
* @param maxGroupSize - per-group cap.
|
|
44
|
+
* @returns the group partition, or null when the call is not a chat tool-call node.
|
|
45
|
+
*/
|
|
46
|
+
export declare function readRun(order: readonly string[], nodes: ChatNodeStore, myCallId: string, tools: readonly string[], groupBy: MergeGroupMode, maxGroupSize: number): ReadRun | null;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ToolCallBlock } from '@deepseek-ai/dsh-client-runtime/client';
|
|
2
|
+
import type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client';
|
|
3
|
+
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
|
|
4
|
+
import type { MergeToolCallsConfig } from '../types.ts';
|
|
5
|
+
/** Locale seat and inject face of the merged row registration. */
|
|
6
|
+
export type MergedToolRowProps = ToolCallViewProps & PropsLocale<'merge-tool-calls'> & {
|
|
7
|
+
readonly cfg: MergeToolCallsConfig;
|
|
8
|
+
};
|
|
9
|
+
/** The run's main card: the first call's full row (chrome + expandable card). */
|
|
10
|
+
export declare const RowCard: import("react").NamedExoticComponent<{
|
|
11
|
+
toolName: string;
|
|
12
|
+
block: ToolCallBlock;
|
|
13
|
+
cwd: string | undefined;
|
|
14
|
+
openFile: (path: string) => void;
|
|
15
|
+
inspect: (() => void) | undefined;
|
|
16
|
+
t: MergedToolRowProps["t"];
|
|
17
|
+
/** Continuation-call count rendered as a muted `+n` suffix; 0 hides it. */
|
|
18
|
+
mergedCount: number;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* One compact continuation row: the main row's tail structure ([sep dot][path]).
|
|
22
|
+
* An expandable call toggles the inline content card on click (mirroring the
|
|
23
|
+
* main row's whole-row disclosure); a read/write/edit-family path additionally
|
|
24
|
+
* renders as an open-file link (the sidebar preview) that stops propagation,
|
|
25
|
+
* exactly like the main row's summary link.
|
|
26
|
+
*/
|
|
27
|
+
export declare const ChildRow: import("react").NamedExoticComponent<{
|
|
28
|
+
toolName: string;
|
|
29
|
+
block: ToolCallBlock;
|
|
30
|
+
cwd: string | undefined;
|
|
31
|
+
openFile: (path: string) => void;
|
|
32
|
+
t: MergedToolRowProps["t"];
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* The shadowed toolview: renders the merged run card for the run's first call,
|
|
36
|
+
* nothing for continuation calls, and a plain single row when this call is not
|
|
37
|
+
* a chat tool-call node.
|
|
38
|
+
*
|
|
39
|
+
* Child-row alignment is measured at runtime, not hardcoded: the main row's
|
|
40
|
+
* separator dot sits after a variable-width title ("Read"/"Search"/"Bash"/…),
|
|
41
|
+
* so its column depends on the rendered font. A layout effect measures the
|
|
42
|
+
* dot's offset from the card root (once, plus on reflow via ResizeObserver)
|
|
43
|
+
* and indents the children so their dots and paths land on the main row's
|
|
44
|
+
* columns — no font/title constants to keep in sync.
|
|
45
|
+
*/
|
|
46
|
+
export declare function MergedToolRow({ callId, toolName, block, cwd, openFile, inspect, t, cfg, useSession }: MergedToolRowProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One scoped stylesheet injected for the lifetime of the client activation.
|
|
3
|
+
*
|
|
4
|
+
* Two jobs:
|
|
5
|
+
* 1. The empty-seat collapse rule: the shadowed toolview renders `null` for
|
|
6
|
+
* every non-first call of a merged run. The built-in `ToolCallTree` still
|
|
7
|
+
* wraps that null in its `.callRow` div, and the renderer wraps every
|
|
8
|
+
* toolview in an empty `<div data-slot="tool.call.toolview">` (display:
|
|
9
|
+
* contents), so the seat's `.flowItem` is never `:empty` itself and the
|
|
10
|
+
* built-in `.flowItem:empty { display:none }` rule does not fire. This
|
|
11
|
+
* rule extends the same intent ("a renderer may decline its row") to a
|
|
12
|
+
* tool seat whose toolview rendered nothing.
|
|
13
|
+
* 2. The plugin's own `.mtc-*` chrome for the merged card and its child rows.
|
|
14
|
+
*
|
|
15
|
+
* All colors come from the shared `--dsw-*` tokens (never literals).
|
|
16
|
+
*/
|
|
17
|
+
export declare const CSS = "\n/* A tool-call seat whose toolview declined to render (merged-run continuation\n calls) must not consume the flow column's gap. The empty toolview slot\n wrapper is the decline signal; the callRow around it always exists. */\n[data-chat-flow-kind=\"tool-call\"]:has([data-slot=\"tool.call.toolview\"]:empty) { display: none; }\n\n.mtc-root { display: flex; flex-direction: column; min-width: 0; }\n.mtc-row { display: flex; flex-direction: column; min-width: 0; }\n.mtc-row[data-state='running'] .mtc-title-row { position: relative; overflow: hidden; }\n.mtc-row[data-state='running'] .mtc-title-row::after {\n content: '';\n position: absolute;\n top: 0; bottom: 0; left: 0;\n width: 300px;\n background: linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, transparent 100%);\n animation: dsh-mtc-sweep 2.6s ease-out infinite;\n pointer-events: none;\n}\n@keyframes dsh-mtc-sweep {\n 0% { left: -300px; }\n 90%, 100% { left: 100%; }\n}\n\n.mtc-title { font-weight: 400; }\n.mtc-sep { flex: none; width: 2px; height: 2px; border-radius: 1px; margin: 0 8px; background: var(--dsw-alias-label-caption); }\n.mtc-summary {\n flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n font-size: 14px; line-height: 24px; color: var(--dsw-alias-label-tertiary);\n}\n.mtc-summary-link {\n flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n margin: 0; padding: 0; border: none; background: none; font: inherit; text-align: left;\n font-size: 14px; line-height: 24px; color: var(--dsw-alias-label-secondary);\n text-decoration: underline; text-decoration-color: var(--dsw-alias-label-quaternary);\n text-underline-offset: 3px; cursor: pointer;\n}\n.mtc-summary-link:hover { color: var(--dsw-alias-label-primary); text-decoration-color: currentColor; }\n.mtc-summary-error { color: var(--dsw-alias-state-error-primary); }\n.mtc-summary-suffix { flex: none; margin-left: 4px; white-space: nowrap; font-size: 14px; line-height: 24px; color: var(--dsw-alias-label-tertiary); }\n\n.mtc-card-body { margin: 4px 0 4px 4px; min-width: 0; }\n.mtc-recovery { margin: 4px 0 4px 4px; white-space: pre-wrap; overflow-wrap: anywhere; font: var(--dsw-font-xs-13); color: var(--dsw-alias-label-tertiary); }\n/* IN/OUT text card for calls without a card primitive (mirrors ToolRow). */\n.mtc-io-card {\n display: flex; flex-direction: column; min-width: 0;\n border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px;\n background: var(--dsw-alias-bg-base);\n}\n.mtc-io-section { display: flex; gap: 10px; padding: 8px 12px; min-width: 0; }\n.mtc-io-label {\n flex: none; font-size: 11px; line-height: 16px; font-weight: 600;\n color: var(--dsw-alias-label-tertiary);\n}\n.mtc-io-text {\n flex: 1 1 auto; min-width: 0; white-space: pre-wrap; overflow-wrap: anywhere;\n font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary);\n}\n.mtc-io-text[data-error] { color: var(--dsw-alias-state-error-primary); }\n.mtc-io-divider { height: 1px; background: var(--dsw-alias-border-l2); }\n.mtc-inspect {\n display: inline-flex; align-self: flex-start; align-items: center; gap: 4px;\n margin: 4px 0 2px 4px; padding: 2px 8px;\n border: 1px solid var(--dsw-alias-border-l2); border-radius: 999px;\n background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-secondary);\n font-size: 11px; line-height: 16px; cursor: pointer;\n opacity: 0; transition: opacity 100ms ease;\n}\n.mtc-root:hover .mtc-inspect, .mtc-inspect:focus-visible { opacity: 1; }\n.mtc-inspect:hover { background: var(--dsw-alias-interactive-bg-hover-solid); color: var(--dsw-alias-label-primary); }\n\n/* Compact child rows: each is the main row's tail structure \u2014 [sep dot][path].\n The dot/path columns are set at runtime by the component (it measures the\n main row's sep offset and sets the --mtc-sep-left custom property on the\n children block), so this is only the pre-measure fallback. */\n.mtc-children { display: flex; flex-direction: column; gap: 1px; margin: 2px 0 2px var(--mtc-sep-left, 61px); }\n.mtc-child { display: flex; flex-direction: column; min-width: 0; }\n.mtc-child-row {\n display: flex; align-items: center; gap: 0; min-width: 0; height: 20px;\n margin: 0; padding: 0; border: 0; border-radius: 4px; background: none;\n font: inherit; text-align: left; color: var(--dsw-alias-label-secondary); cursor: pointer;\n}\n.mtc-child-row:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }\n.mtc-child-row[data-static] { cursor: default; }\n.mtc-child-row[data-static]:hover { background: none; color: var(--dsw-alias-label-secondary); }\n.mtc-child-row .mtc-sep { margin: 0 8px 0 0; }\n.mtc-child-path {\n flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n font-size: 14px; line-height: 20px;\n}\n/* Read-family child paths are open-file links (sidebar preview), same affordance\n as the main row's summary link. */\n.mtc-child-path-link {\n flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n margin: 0; padding: 0; border: none; background: none; font: inherit; text-align: left;\n font-size: 14px; line-height: 20px; color: var(--dsw-alias-label-secondary);\n text-decoration: underline; text-decoration-color: var(--dsw-alias-label-quaternary);\n text-underline-offset: 3px; cursor: pointer;\n}\n.mtc-child-path-link:hover { color: var(--dsw-alias-label-primary); text-decoration-color: currentColor; }\n.mtc-child-state { flex: none; font-size: 11px; line-height: 16px; color: var(--dsw-alias-label-tertiary); }\n.mtc-child-state[data-error] { color: var(--dsw-alias-state-error-primary); }\n/* The expanded child card must not inherit the children block's sep indent:\n pull its left edge back to the main card body's column (root + 4px) so the\n card spans the card width instead of leaving a blank gutter on its left. */\n.mtc-child-body { margin: 2px 0 2px calc(4px - var(--mtc-sep-left, 61px)); min-width: 0; }\n\n.mtc-visually-hidden {\n position: absolute; width: 1px; height: 1px; overflow: hidden;\n clip: rect(0 0 0 0); white-space: nowrap;\n}\n";
|
|
18
|
+
/** Install the stylesheet and return its disposer. */
|
|
19
|
+
export declare function installStyles(): () => void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in tool universe this plugin shadows by default, plus the
|
|
3
|
+
* variant-classification vocabulary mirroring ui-tool's tool-call model.
|
|
4
|
+
*
|
|
5
|
+
* The keyed `tool.call.toolview` slot dispatches on the exact wire tool name,
|
|
6
|
+
* so a shadowed registration is needed per name (there is no wildcard). This
|
|
7
|
+
* list names every shipped tool whose row belongs to the generic ToolRow
|
|
8
|
+
* family — a variant title/icon plus read/search/diff/terminal/web card or
|
|
9
|
+
* IN/OUT text — which the merged row reproduces faithfully.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately excluded: tools with custom row cards this plugin does not
|
|
12
|
+
* replicate (`skill`, `cordis_define`, and the live `cordis_run` /
|
|
13
|
+
* `cordis_stop` / `cordis_undefine` rows), and `todo_write` /
|
|
14
|
+
* `ask_user_question` whose summary formatting is tool-specific. They keep
|
|
15
|
+
* their built-in rows unless the user names them in the `tools` config
|
|
16
|
+
* explicitly.
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
/** Wire tool names merged by default (empty `tools` config = this list). */
|
|
20
|
+
export declare const ALL_TOOL_NAMES: readonly string[];
|
|
21
|
+
/** Tool-row visual variant, mirroring ui-tool's ToolRowVariant. */
|
|
22
|
+
export type ToolVariant = 'search' | 'read' | 'bash' | 'write' | 'edit' | 'code' | 'others';
|
|
23
|
+
/** Figma row titles per variant (design literals, not translatable copy). */
|
|
24
|
+
export declare const VARIANT_TITLES: Record<ToolVariant, string>;
|
|
25
|
+
/** Tool-owned titles refining a generic row variant (mirrors ui-tool). */
|
|
26
|
+
export declare const TOOL_TITLES: Record<string, string>;
|
|
27
|
+
/** Classify a tool name into its row variant (mirrors ui-tool). */
|
|
28
|
+
export declare function classifyTool(toolName: string): ToolVariant;
|
|
29
|
+
/** The variant title a row shows, honoring tool-owned refinements. */
|
|
30
|
+
export declare function variantTitle(toolName: string): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* merge-tool-calls — host entry.
|
|
3
|
+
*
|
|
4
|
+
* The plugin is browser-only: it shadows the shipped `read`/`grep`/`glob`
|
|
5
|
+
* toolviews inside `tool.call.toolview` (see `src/client/`). The host half
|
|
6
|
+
* exists to declare the validated Config schema for the bundle row; its apply
|
|
7
|
+
* is intentionally empty.
|
|
8
|
+
*
|
|
9
|
+
* @module @huanlin/dsh-plugin-merge-tool-calls
|
|
10
|
+
*/
|
|
11
|
+
import z from '@deepseek-ai/schemastery';
|
|
12
|
+
import type { MergeToolCallsConfig } from './types.ts';
|
|
13
|
+
export declare const name = "merge-tool-calls";
|
|
14
|
+
export type { MergeToolCallsConfig } from './types.ts';
|
|
15
|
+
/** Validated output shape of {@link Config} (defaults applied). */
|
|
16
|
+
export interface Config extends MergeToolCallsConfig {
|
|
17
|
+
}
|
|
18
|
+
/** Schemastery schema: validated Config fields with the row's runtime defaults. */
|
|
19
|
+
export declare const Config: z<Config>;
|
|
20
|
+
/**
|
|
21
|
+
* Empty host apply: all registrations live in the browser half.
|
|
22
|
+
* @param _ctx - cordis context (unused; the plugin contributes client slots only).
|
|
23
|
+
*/
|
|
24
|
+
export declare function apply(_ctx: unknown): void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Package invariant companion for dsh-merge-tool-calls. */
|
|
2
|
+
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants';
|
|
3
|
+
export declare const name = "merge-tool-calls-invariant";
|
|
4
|
+
export declare const inject: string[];
|
|
5
|
+
interface InvariantContext {
|
|
6
|
+
invariants: {
|
|
7
|
+
register: (name: string, installer: InvariantInstaller) => () => void;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/** Register package ownership with the invariant service. */
|
|
11
|
+
export declare const apply: (ctx: InvariantContext) => Promise<() => void>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Shared config surface of the merge-tool-calls plugin (host + client halves). */
|
|
2
|
+
/** How consecutive same-tool calls are grouped into one merged card. */
|
|
3
|
+
export type MergeGroupMode = 'adjacent' | 'step';
|
|
4
|
+
/** Plugin configuration; defaults live in the Schemastery schema and cordis.patch.yml. */
|
|
5
|
+
export interface MergeToolCallsConfig {
|
|
6
|
+
/**
|
|
7
|
+
* Wire tool names whose consecutive calls merge. Empty = every built-in
|
|
8
|
+
* generic-family tool (read/grep/glob/edit/write/bash/pwsh/web_search/
|
|
9
|
+
* web_fetch/run_code/cordis_package_inspect/cordis_runtime_inspect); a
|
|
10
|
+
* non-empty list is an explicit whitelist (any wire name works).
|
|
11
|
+
*/
|
|
12
|
+
readonly tools: readonly string[];
|
|
13
|
+
/** `adjacent`: any consecutive run in the chat flow; `step`: only within one agent step. */
|
|
14
|
+
readonly groupBy: MergeGroupMode;
|
|
15
|
+
/** Max calls per merged group; the run is truncated and the excess starts a new group. */
|
|
16
|
+
readonly maxGroupSize: number;
|
|
17
|
+
}
|
|
18
|
+
/** Runtime defaults applied when a half receives no config (defensive only). */
|
|
19
|
+
export declare const DEFAULT_MERGE_CONFIG: MergeToolCallsConfig;
|
package/package.json
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@huanlin/dsh-plugin-merge-tool-calls",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"keywords": [
|
|
8
|
+
"dsh-plugin"
|
|
9
|
+
],
|
|
10
|
+
"description": "Merge consecutive same-tool calls in the DSH WebUI chat flow into one card with compact child rows (all built-in tools by default).",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"main": "./lib/index.js",
|
|
14
|
+
"types": "./lib/types/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./lib/types/index.d.ts",
|
|
18
|
+
"import": "./lib/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./invariant": {
|
|
21
|
+
"types": "./lib/types/invariant.d.ts",
|
|
22
|
+
"import": "./lib/invariant.js"
|
|
23
|
+
},
|
|
24
|
+
"./client": {
|
|
25
|
+
"types": "./lib/types/client/index.d.ts",
|
|
26
|
+
"default": "./lib/client.js"
|
|
27
|
+
},
|
|
28
|
+
"./src/*": "./src/*",
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"lib/",
|
|
33
|
+
"cordis.patch.yml"
|
|
34
|
+
],
|
|
35
|
+
"dsh": {
|
|
36
|
+
"bundle": {
|
|
37
|
+
"patch": "./cordis.patch.yml"
|
|
38
|
+
},
|
|
39
|
+
"client": {
|
|
40
|
+
"inject": [
|
|
41
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
42
|
+
"@deepseek-ai/dsh-client-locale",
|
|
43
|
+
"@deepseek-ai/dsh-client-ui-tool",
|
|
44
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-primitives"
|
|
46
|
+
],
|
|
47
|
+
"platform": "web"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"cordis": "^4.0.0-rc.7",
|
|
52
|
+
"react": "^18.2.0",
|
|
53
|
+
"schemastery": "^3.18.0",
|
|
54
|
+
"@deepseek-ai/dsh-client-locale": "0.1.0-rc.6",
|
|
55
|
+
"@deepseek-ai/dsh-client-runtime": "0.1.0-rc.6",
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.0-rc.6",
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.6",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-tool": "0.1.0-rc.6"
|
|
59
|
+
},
|
|
60
|
+
"peerDependenciesMeta": {
|
|
61
|
+
"cordis": {
|
|
62
|
+
"optional": true
|
|
63
|
+
},
|
|
64
|
+
"react": {
|
|
65
|
+
"optional": true
|
|
66
|
+
},
|
|
67
|
+
"schemastery": {
|
|
68
|
+
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"@deepseek-ai/dsh-client-locale": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"@deepseek-ai/dsh-client-runtime": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"@deepseek-ai/dsh-client-ui-primitives": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"@deepseek-ai/dsh-client-ui-slots": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"@deepseek-ai/dsh-client-ui-tool": {
|
|
83
|
+
"optional": true
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"devDependencies": {
|
|
87
|
+
"@deepseek-ai/dsh-client-locale": "0.1.0-rc.6",
|
|
88
|
+
"@deepseek-ai/dsh-client-runtime": "0.1.0-rc.6",
|
|
89
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.1.0-rc.6",
|
|
90
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.0-rc.6",
|
|
91
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.6",
|
|
92
|
+
"@deepseek-ai/dsh-client-ui-tool": "0.1.0-rc.6",
|
|
93
|
+
"@deepseek-ai/dsh-invariants": "0.1.0-rc.6",
|
|
94
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
95
|
+
"@types/node": "^22.20.1",
|
|
96
|
+
"@types/react": "~18.3.31",
|
|
97
|
+
"cordis": "^4.0.0-rc.7",
|
|
98
|
+
"jsdom": "^26.0.0",
|
|
99
|
+
"react": "^18.2.0",
|
|
100
|
+
"react-dom": "^18.2.0",
|
|
101
|
+
"schemastery": "^3.18.0",
|
|
102
|
+
"tsdown": "^0.15.0",
|
|
103
|
+
"typescript": "^5.9.0",
|
|
104
|
+
"vitest": "^3.2.0"
|
|
105
|
+
},
|
|
106
|
+
"engines": {
|
|
107
|
+
"node": ">=22.0.0"
|
|
108
|
+
},
|
|
109
|
+
"scripts": {
|
|
110
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
111
|
+
"test": "vitest run",
|
|
112
|
+
"build": "tsdown -c tsdown.config.ts && tsc -p tsconfig.json"
|
|
113
|
+
}
|
|
114
|
+
}
|