@stigmer/react 3.12.2 → 3.12.3
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/access/ManageAccessDialog.d.ts.map +1 -1
- package/access/ManageAccessDialog.js +3 -19
- package/access/ManageAccessDialog.js.map +1 -1
- package/agent-instance/CreateAgentInstanceDialog.d.ts.map +1 -1
- package/agent-instance/CreateAgentInstanceDialog.js +7 -20
- package/agent-instance/CreateAgentInstanceDialog.js.map +1 -1
- package/attachment/AttachmentImageLightbox.d.ts.map +1 -1
- package/attachment/AttachmentImageLightbox.js +8 -23
- package/attachment/AttachmentImageLightbox.js.map +1 -1
- package/channel/ChannelConversationsDialog.d.ts.map +1 -1
- package/channel/ChannelConversationsDialog.js +3 -19
- package/channel/ChannelConversationsDialog.js.map +1 -1
- package/channel/ChannelCredentialsDialog.d.ts.map +1 -1
- package/channel/ChannelCredentialsDialog.js +3 -19
- package/channel/ChannelCredentialsDialog.js.map +1 -1
- package/channel/ChannelTemplatesDialog.d.ts.map +1 -1
- package/channel/ChannelTemplatesDialog.js +4 -20
- package/channel/ChannelTemplatesDialog.js.map +1 -1
- package/channel/ConnectSlackDialog.d.ts.map +1 -1
- package/channel/ConnectSlackDialog.js +3 -21
- package/channel/ConnectSlackDialog.js.map +1 -1
- package/channel/ConnectWhatsAppDialog.d.ts.map +1 -1
- package/channel/ConnectWhatsAppDialog.js +3 -21
- package/channel/ConnectWhatsAppDialog.js.map +1 -1
- package/conversation/ConversationAttentionBanner.d.ts +23 -6
- package/conversation/ConversationAttentionBanner.d.ts.map +1 -1
- package/conversation/ConversationAttentionBanner.js +25 -10
- package/conversation/ConversationAttentionBanner.js.map +1 -1
- package/conversation/ConversationListPane.d.ts.map +1 -1
- package/conversation/ConversationListPane.js +25 -6
- package/conversation/ConversationListPane.js.map +1 -1
- package/conversation/ConversationsWorkbench.d.ts.map +1 -1
- package/conversation/ConversationsWorkbench.js +1 -1
- package/conversation/ConversationsWorkbench.js.map +1 -1
- package/execution/ApprovalCard.d.ts.map +1 -1
- package/execution/ApprovalCard.js +13 -3
- package/execution/ApprovalCard.js.map +1 -1
- package/execution/ArtifactPreviewModal.d.ts.map +1 -1
- package/execution/ArtifactPreviewModal.js +11 -17
- package/execution/ArtifactPreviewModal.js.map +1 -1
- package/execution/ToolCallItem.d.ts +3 -1
- package/execution/ToolCallItem.d.ts.map +1 -1
- package/execution/ToolCallItem.js +19 -7
- package/execution/ToolCallItem.js.map +1 -1
- package/execution/index.d.ts +1 -1
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +1 -1
- package/execution/index.js.map +1 -1
- package/execution/tool-categories.d.ts +51 -0
- package/execution/tool-categories.d.ts.map +1 -1
- package/execution/tool-categories.js +103 -1
- package/execution/tool-categories.js.map +1 -1
- package/execution/tool-presenter.d.ts +15 -0
- package/execution/tool-presenter.d.ts.map +1 -1
- package/execution/tool-presenter.js +7 -2
- package/execution/tool-presenter.js.map +1 -1
- package/index.d.ts +1 -1
- package/index.d.ts.map +1 -1
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/internal/DialogShell.d.ts +77 -0
- package/internal/DialogShell.d.ts.map +1 -0
- package/internal/DialogShell.js +77 -0
- package/internal/DialogShell.js.map +1 -0
- package/manifest/ApplyManifestDialog.d.ts.map +1 -1
- package/manifest/ApplyManifestDialog.js +7 -17
- package/manifest/ApplyManifestDialog.js.map +1 -1
- package/manifest/EditResourceYamlDialog.d.ts.map +1 -1
- package/manifest/EditResourceYamlDialog.js +3 -19
- package/manifest/EditResourceYamlDialog.js.map +1 -1
- package/mcp-server/McpServerConnectDialog.d.ts.map +1 -1
- package/mcp-server/McpServerConnectDialog.js +8 -20
- package/mcp-server/McpServerConnectDialog.js.map +1 -1
- package/mcp-server/McpServerDetailView.d.ts.map +1 -1
- package/mcp-server/McpServerDetailView.js +4 -15
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/package.json +4 -4
- package/resource-detail/ConfirmDialog.d.ts +2 -3
- package/resource-detail/ConfirmDialog.d.ts.map +1 -1
- package/resource-detail/ConfirmDialog.js +8 -20
- package/resource-detail/ConfirmDialog.js.map +1 -1
- package/sharing/ShareAgentDialog.d.ts.map +1 -1
- package/sharing/ShareAgentDialog.js +2 -20
- package/sharing/ShareAgentDialog.js.map +1 -1
- package/skill/SkillDiffDialog.d.ts.map +1 -1
- package/skill/SkillDiffDialog.js +5 -21
- package/skill/SkillDiffDialog.js.map +1 -1
- package/src/access/ManageAccessDialog.tsx +7 -27
- package/src/agent-instance/CreateAgentInstanceDialog.tsx +14 -27
- package/src/attachment/AttachmentImageLightbox.tsx +13 -36
- package/src/channel/ChannelConversationsDialog.tsx +8 -27
- package/src/channel/ChannelCredentialsDialog.tsx +8 -28
- package/src/channel/ChannelTemplatesDialog.tsx +10 -29
- package/src/channel/ConnectSlackDialog.tsx +8 -30
- package/src/channel/ConnectWhatsAppDialog.tsx +8 -30
- package/src/conversation/ConversationAttentionBanner.tsx +64 -19
- package/src/conversation/ConversationListPane.tsx +42 -17
- package/src/conversation/ConversationsWorkbench.tsx +1 -0
- package/src/conversation/__tests__/ConversationAttentionBanner.test.tsx +76 -2
- package/src/conversation/__tests__/ConversationListPane.test.tsx +15 -4
- package/src/execution/ApprovalCard.tsx +23 -2
- package/src/execution/ArtifactPreviewModal.tsx +15 -27
- package/src/execution/ToolCallItem.tsx +19 -8
- package/src/execution/__tests__/intent-title.test.tsx +221 -0
- package/src/execution/index.ts +3 -0
- package/src/execution/tool-categories.ts +123 -1
- package/src/execution/tool-presenter.ts +22 -1
- package/src/index.ts +3 -0
- package/src/internal/DialogShell.tsx +169 -0
- package/src/internal/__tests__/DialogShell.test.tsx +223 -0
- package/src/manifest/ApplyManifestDialog.tsx +8 -26
- package/src/manifest/EditResourceYamlDialog.tsx +4 -31
- package/src/mcp-server/McpServerConnectDialog.tsx +15 -29
- package/src/mcp-server/McpServerDetailView.tsx +12 -24
- package/src/resource-detail/ConfirmDialog.tsx +9 -30
- package/src/sharing/ShareAgentDialog.tsx +7 -29
- package/src/skill/SkillDiffDialog.tsx +13 -36
- package/src/workflow/ViewYamlDialog.tsx +18 -37
- package/src/workflow/WorkflowArchitectDialog.tsx +11 -43
- package/src/workflow/WorkflowExplainDialog.tsx +11 -37
- package/src/workflow/WorkflowGraphFullscreenDialog.tsx +14 -26
- package/src/workflow/WorkflowRunDialog.tsx +21 -48
- package/src/workflow/execution-comparison/ExecutionComparisonPicker.tsx +14 -31
- package/src/workflow/instance/CreateWorkflowInstanceDialog.tsx +14 -27
- package/src/workflow/templates/WorkflowTemplatePreview.tsx +18 -28
- package/styles.css +1 -1
- package/workflow/ViewYamlDialog.d.ts.map +1 -1
- package/workflow/ViewYamlDialog.js +10 -29
- package/workflow/ViewYamlDialog.js.map +1 -1
- package/workflow/WorkflowArchitectDialog.d.ts.map +1 -1
- package/workflow/WorkflowArchitectDialog.js +5 -24
- package/workflow/WorkflowArchitectDialog.js.map +1 -1
- package/workflow/WorkflowExplainDialog.d.ts.map +1 -1
- package/workflow/WorkflowExplainDialog.js +5 -21
- package/workflow/WorkflowExplainDialog.js.map +1 -1
- package/workflow/WorkflowGraphFullscreenDialog.d.ts.map +1 -1
- package/workflow/WorkflowGraphFullscreenDialog.js +11 -17
- package/workflow/WorkflowGraphFullscreenDialog.js.map +1 -1
- package/workflow/WorkflowRunDialog.d.ts.map +1 -1
- package/workflow/WorkflowRunDialog.js +13 -29
- package/workflow/WorkflowRunDialog.js.map +1 -1
- package/workflow/execution-comparison/ExecutionComparisonPicker.d.ts.map +1 -1
- package/workflow/execution-comparison/ExecutionComparisonPicker.js +8 -26
- package/workflow/execution-comparison/ExecutionComparisonPicker.js.map +1 -1
- package/workflow/instance/CreateWorkflowInstanceDialog.d.ts.map +1 -1
- package/workflow/instance/CreateWorkflowInstanceDialog.js +7 -20
- package/workflow/instance/CreateWorkflowInstanceDialog.js.map +1 -1
- package/workflow/templates/WorkflowTemplatePreview.d.ts +1 -1
- package/workflow/templates/WorkflowTemplatePreview.d.ts.map +1 -1
- package/workflow/templates/WorkflowTemplatePreview.js +10 -22
- package/workflow/templates/WorkflowTemplatePreview.js.map +1 -1
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import {
|
|
13
13
|
resolveToolCategoryFromKind,
|
|
14
14
|
extractPrimaryArgFromPreview,
|
|
15
|
+
extractShellIntentFromPreview,
|
|
15
16
|
extractWriteContentFromPreview,
|
|
16
17
|
isFileCategory,
|
|
17
18
|
type ToolCategory,
|
|
@@ -160,6 +161,26 @@ export function ApprovalCardHeader({
|
|
|
160
161
|
],
|
|
161
162
|
);
|
|
162
163
|
|
|
164
|
+
// A shell gate whose call carries a model-authored intent phrase titles
|
|
165
|
+
// the header with it (stigmer#276) — supplementary context only: the
|
|
166
|
+
// command stays foregrounded in the body's terminal preview, which remains
|
|
167
|
+
// the content the user is approving.
|
|
168
|
+
const intent = useMemo(
|
|
169
|
+
() =>
|
|
170
|
+
extractShellIntentFromPreview(
|
|
171
|
+
pendingApproval.toolName,
|
|
172
|
+
pendingApproval.argsPreview,
|
|
173
|
+
pendingApproval.mcpServerSlug,
|
|
174
|
+
pendingApproval.toolKind,
|
|
175
|
+
),
|
|
176
|
+
[
|
|
177
|
+
pendingApproval.toolName,
|
|
178
|
+
pendingApproval.argsPreview,
|
|
179
|
+
pendingApproval.mcpServerSlug,
|
|
180
|
+
pendingApproval.toolKind,
|
|
181
|
+
],
|
|
182
|
+
);
|
|
183
|
+
|
|
163
184
|
return (
|
|
164
185
|
<div
|
|
165
186
|
className={cn(
|
|
@@ -173,7 +194,7 @@ export function ApprovalCardHeader({
|
|
|
173
194
|
|
|
174
195
|
<span className="stg:min-w-0 stg:flex-1 stg:flex stg:items-center stg:gap-1.5 stg:overflow-hidden">
|
|
175
196
|
<span className="stg:shrink-0 stg:font-medium stg:text-foreground">
|
|
176
|
-
{categoryInfo.label}
|
|
197
|
+
{intent ?? categoryInfo.label}
|
|
177
198
|
</span>
|
|
178
199
|
{primaryArg &&
|
|
179
200
|
categoryInfo.category !== "shell" &&
|
|
@@ -186,7 +207,7 @@ export function ApprovalCardHeader({
|
|
|
186
207
|
/>
|
|
187
208
|
) : (
|
|
188
209
|
// Shell is the exception: its command is shown in the body's
|
|
189
|
-
// terminal session, so the header stays minimal (icon +
|
|
210
|
+
// terminal session, so the header stays minimal (icon + title).
|
|
190
211
|
<span className="stg:min-w-0 stg:truncate stg:font-mono stg:text-muted-foreground">
|
|
191
212
|
{primaryArg}
|
|
192
213
|
</span>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useCallback
|
|
3
|
+
import { useCallback } from "react";
|
|
4
4
|
import type { ExecutionArtifact } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
5
5
|
import { cn } from "@stigmer/theme";
|
|
6
|
+
import { DialogShell } from "../internal/DialogShell.js";
|
|
6
7
|
import { useArtifactDownload } from "./useArtifactDownload.js";
|
|
7
8
|
import { formatArtifactSize } from "./artifact-utils.js";
|
|
8
9
|
import { ArtifactContentBody } from "./ArtifactContentBody.js";
|
|
@@ -253,37 +254,24 @@ export function ArtifactPreviewModal({
|
|
|
253
254
|
onImplement,
|
|
254
255
|
className,
|
|
255
256
|
}: ArtifactPreviewModalProps) {
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const dialog = dialogRef.current;
|
|
260
|
-
if (!dialog) return;
|
|
261
|
-
|
|
262
|
-
if (open && !dialog.open) {
|
|
263
|
-
dialog.showModal();
|
|
264
|
-
} else if (!open && dialog.open) {
|
|
265
|
-
dialog.close();
|
|
266
|
-
}
|
|
267
|
-
}, [open]);
|
|
268
|
-
|
|
269
|
-
const handleCancel = useCallback(
|
|
270
|
-
(e: React.SyntheticEvent) => {
|
|
271
|
-
e.preventDefault();
|
|
272
|
-
onClose();
|
|
257
|
+
const handleOpenChange = useCallback(
|
|
258
|
+
(nextOpen: boolean) => {
|
|
259
|
+
if (!nextOpen) onClose();
|
|
273
260
|
},
|
|
274
261
|
[onClose],
|
|
275
262
|
);
|
|
276
263
|
|
|
277
264
|
return (
|
|
278
|
-
<
|
|
279
|
-
|
|
280
|
-
|
|
265
|
+
<DialogShell
|
|
266
|
+
open={open}
|
|
267
|
+
onOpenChange={handleOpenChange}
|
|
268
|
+
width="3xl"
|
|
281
269
|
aria-label={`Preview ${artifact.name}`}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
)}
|
|
270
|
+
// Background surface is deliberate for content previews. The
|
|
271
|
+
// previously hardcoded ::backdrop color (bg-black/50 — it escaped the
|
|
272
|
+
// #652 fence via the arbitrary-variant spelling) now rides the
|
|
273
|
+
// shell's token backdrop.
|
|
274
|
+
className={cn("stg:bg-background stg:text-foreground stg:outline-none", className)}
|
|
287
275
|
>
|
|
288
276
|
{open && (
|
|
289
277
|
<ArtifactPreviewContent
|
|
@@ -296,7 +284,7 @@ export function ArtifactPreviewModal({
|
|
|
296
284
|
onImplement={onImplement}
|
|
297
285
|
/>
|
|
298
286
|
)}
|
|
299
|
-
</
|
|
287
|
+
</DialogShell>
|
|
300
288
|
);
|
|
301
289
|
}
|
|
302
290
|
|
|
@@ -105,7 +105,9 @@ export interface ToolCallItemProps {
|
|
|
105
105
|
*
|
|
106
106
|
* Shows category-aware labels (e.g., "Shell", "Read", "Edit") with
|
|
107
107
|
* the primary argument as a subtitle, a category-specific icon, and
|
|
108
|
-
* an inline approval decision badge when applicable.
|
|
108
|
+
* an inline approval decision badge when applicable. A shell row whose
|
|
109
|
+
* call carries a model-authored intent phrase is titled with that phrase
|
|
110
|
+
* instead — the command becomes the subtitle (stigmer#276).
|
|
109
111
|
*
|
|
110
112
|
* Wrapped in `React.memo` — structural sharing (DD-009/010) preserves
|
|
111
113
|
* the `ToolCall` reference when unchanged, so a settled row skips
|
|
@@ -138,7 +140,7 @@ export const ToolCallItem = memo(function ToolCallItem({
|
|
|
138
140
|
const duration = formatHeaderDuration(toolCall.startedAt, toolCall.completedAt);
|
|
139
141
|
const isSubAgent = subAgentExecution != null;
|
|
140
142
|
|
|
141
|
-
const { category, label, primaryArg, result, resultSummary, disclosure, chrome } =
|
|
143
|
+
const { category, label, intent, title, primaryArg, result, resultSummary, disclosure, chrome } =
|
|
142
144
|
useToolPresentation(toolCall);
|
|
143
145
|
const CategoryIcon = CATEGORY_ICON[category];
|
|
144
146
|
|
|
@@ -173,9 +175,12 @@ export const ToolCallItem = memo(function ToolCallItem({
|
|
|
173
175
|
initialOpen: defaultExpanded || autoOpen,
|
|
174
176
|
});
|
|
175
177
|
|
|
178
|
+
// Sub-agents title the row with their subject; every other row titles with
|
|
179
|
+
// the resolved presentation `title` (host label override > model-authored
|
|
180
|
+
// shell intent > category label, stigmer#276).
|
|
176
181
|
const displayLabel = isSubAgent
|
|
177
182
|
? subAgentExecution.subject || subAgentExecution.name || label
|
|
178
|
-
:
|
|
183
|
+
: title;
|
|
179
184
|
|
|
180
185
|
const approvalBadge = getApprovalBadge(toolCall);
|
|
181
186
|
|
|
@@ -308,11 +313,17 @@ export const ToolCallItem = memo(function ToolCallItem({
|
|
|
308
313
|
);
|
|
309
314
|
}
|
|
310
315
|
|
|
311
|
-
// Shell
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
|
|
315
|
-
|
|
316
|
+
// Shell headers stay minimal (icon + label + status + duration) — the
|
|
317
|
+
// command and exit summary live in the terminal session body — EXCEPT when
|
|
318
|
+
// the model authored an intent title (stigmer#276): the title then says
|
|
319
|
+
// *why*, so the command surfaces as the secondary text and a fully
|
|
320
|
+
// collapsed row still reads like a narrated log. Intent-less shell rows
|
|
321
|
+
// render byte-identically to before.
|
|
322
|
+
const displaySubtitle = isSubAgent
|
|
323
|
+
? null
|
|
324
|
+
: category === "shell"
|
|
325
|
+
? (intent ? primaryArg : null)
|
|
326
|
+
: primaryArg;
|
|
316
327
|
|
|
317
328
|
// For file tools the subtitle is a path: render it filename-first through
|
|
318
329
|
// FilePathLink (full path on hover) rather than the raw, often-absolute string
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-authored intent titles for shell tool calls (stigmer#276).
|
|
3
|
+
*
|
|
4
|
+
* The extraction contract is pinned by the cross-surface fixture
|
|
5
|
+
* test/fixtures/tool-view/intent-title.json — the same file the runner's
|
|
6
|
+
* tool-intent middleware tests assert against, so the wire key and the
|
|
7
|
+
* kind-scoping can never drift between the writer and the readers.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { dirname, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
14
|
+
import { render, screen, cleanup } from "@testing-library/react";
|
|
15
|
+
import { renderHook } from "@testing-library/react";
|
|
16
|
+
import { create, type JsonObject } from "@bufbuild/protobuf";
|
|
17
|
+
import {
|
|
18
|
+
ToolCallSchema,
|
|
19
|
+
type ToolCall,
|
|
20
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
21
|
+
import { PendingApprovalSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
|
|
22
|
+
import { ToolCallStatus, ToolKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
23
|
+
import {
|
|
24
|
+
extractShellIntent,
|
|
25
|
+
extractShellIntentFromPreview,
|
|
26
|
+
SHELL_INTENT_ARG_FIELD,
|
|
27
|
+
} from "../tool-categories";
|
|
28
|
+
import { useToolPresentation, registerToolPresenter } from "../tool-presenter";
|
|
29
|
+
import { ToolCallItem } from "../ToolCallItem";
|
|
30
|
+
import { ApprovalCardHeader } from "../ApprovalCard";
|
|
31
|
+
|
|
32
|
+
afterEach(cleanup);
|
|
33
|
+
|
|
34
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const fixturePath = resolve(here, "../../../../../test/fixtures/tool-view/intent-title.json");
|
|
36
|
+
|
|
37
|
+
interface IntentFixture {
|
|
38
|
+
argField: string;
|
|
39
|
+
cases: Array<{
|
|
40
|
+
name: string;
|
|
41
|
+
mcpServerSlug: string;
|
|
42
|
+
args: Record<string, unknown>;
|
|
43
|
+
intent: string | null;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const fixture = JSON.parse(readFileSync(fixturePath, "utf8")) as IntentFixture;
|
|
48
|
+
|
|
49
|
+
function makeToolCall(opts: {
|
|
50
|
+
name: string;
|
|
51
|
+
args?: Record<string, unknown>;
|
|
52
|
+
mcpServerSlug?: string;
|
|
53
|
+
status?: ToolCallStatus;
|
|
54
|
+
}): ToolCall {
|
|
55
|
+
return create(ToolCallSchema, {
|
|
56
|
+
id: opts.name,
|
|
57
|
+
name: opts.name,
|
|
58
|
+
args: (opts.args ?? {}) as JsonObject,
|
|
59
|
+
mcpServerSlug: opts.mcpServerSlug ?? "",
|
|
60
|
+
status: opts.status ?? ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("intent-title fixture contract", () => {
|
|
65
|
+
it("pins the wire key shared with the runner middleware", () => {
|
|
66
|
+
expect(SHELL_INTENT_ARG_FIELD).toBe(fixture.argField);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it.each(fixture.cases.map((c, i) => [i, c] as const))(
|
|
70
|
+
"case %#: %j",
|
|
71
|
+
(_i, c) => {
|
|
72
|
+
const tc = makeToolCall({
|
|
73
|
+
name: c.name,
|
|
74
|
+
args: c.args,
|
|
75
|
+
mcpServerSlug: c.mcpServerSlug,
|
|
76
|
+
});
|
|
77
|
+
expect(extractShellIntent(tc)).toBe(c.intent);
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("extractShellIntentFromPreview", () => {
|
|
83
|
+
it("mirrors the ToolCall extraction on a JSON args preview", () => {
|
|
84
|
+
expect(
|
|
85
|
+
extractShellIntentFromPreview(
|
|
86
|
+
"execute",
|
|
87
|
+
JSON.stringify({ command: "make check", [SHELL_INTENT_ARG_FIELD]: "Verify the build" }),
|
|
88
|
+
),
|
|
89
|
+
).toBe("Verify the build");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("prefers the denormalized wire kind over the name", () => {
|
|
93
|
+
// A shell tool whose NAME the classifier does not recognize still
|
|
94
|
+
// resolves via the wire kind — matching extractPrimaryArgFromPreview.
|
|
95
|
+
expect(
|
|
96
|
+
extractShellIntentFromPreview(
|
|
97
|
+
"custom_runner",
|
|
98
|
+
JSON.stringify({ command: "ls", [SHELL_INTENT_ARG_FIELD]: "List files" }),
|
|
99
|
+
"",
|
|
100
|
+
ToolKind.SHELL,
|
|
101
|
+
),
|
|
102
|
+
).toBe("List files");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("returns null for malformed previews and non-shell kinds", () => {
|
|
106
|
+
expect(extractShellIntentFromPreview("execute", "not json{")).toBeNull();
|
|
107
|
+
expect(extractShellIntentFromPreview("execute", "")).toBeNull();
|
|
108
|
+
expect(
|
|
109
|
+
extractShellIntentFromPreview(
|
|
110
|
+
"read_file",
|
|
111
|
+
JSON.stringify({ file_path: "a", [SHELL_INTENT_ARG_FIELD]: "x" }),
|
|
112
|
+
),
|
|
113
|
+
).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("useToolPresentation title precedence", () => {
|
|
118
|
+
it("titles a shell row with the model-authored intent", () => {
|
|
119
|
+
const tc = makeToolCall({
|
|
120
|
+
name: "Shell",
|
|
121
|
+
args: { command: "npm test", [SHELL_INTENT_ARG_FIELD]: "Run the test suite" },
|
|
122
|
+
});
|
|
123
|
+
const { result } = renderHook(() => useToolPresentation(tc));
|
|
124
|
+
|
|
125
|
+
expect(result.current.intent).toBe("Run the test suite");
|
|
126
|
+
expect(result.current.title).toBe("Run the test suite");
|
|
127
|
+
// The short kind label is unchanged — chips and badges keep "Shell".
|
|
128
|
+
expect(result.current.label).toBe("Shell");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("falls back to the category label when no intent is present", () => {
|
|
132
|
+
const tc = makeToolCall({ name: "Shell", args: { command: "ls" } });
|
|
133
|
+
const { result } = renderHook(() => useToolPresentation(tc));
|
|
134
|
+
|
|
135
|
+
expect(result.current.intent).toBeNull();
|
|
136
|
+
expect(result.current.title).toBe("Shell");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("a registered presenter label override outranks the intent", () => {
|
|
140
|
+
const dispose = registerToolPresenter(ToolKind.SHELL, {
|
|
141
|
+
label: () => "Run command",
|
|
142
|
+
});
|
|
143
|
+
try {
|
|
144
|
+
const tc = makeToolCall({
|
|
145
|
+
name: "Shell",
|
|
146
|
+
args: { command: "ls", [SHELL_INTENT_ARG_FIELD]: "List files" },
|
|
147
|
+
});
|
|
148
|
+
const { result } = renderHook(() => useToolPresentation(tc));
|
|
149
|
+
|
|
150
|
+
expect(result.current.title).toBe("Run command");
|
|
151
|
+
expect(result.current.intent).toBe("List files");
|
|
152
|
+
} finally {
|
|
153
|
+
dispose();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("ToolCallItem shell header", () => {
|
|
159
|
+
function headerOf(container: HTMLElement): Element {
|
|
160
|
+
const row = container.querySelector('[data-cursor-target="tool-call-row"]')!;
|
|
161
|
+
return row.firstElementChild!;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
it("renders the intent as the title and the command as the subtitle", () => {
|
|
165
|
+
const tc = makeToolCall({
|
|
166
|
+
name: "Shell",
|
|
167
|
+
args: {
|
|
168
|
+
command: "npx vitest run src/parser",
|
|
169
|
+
[SHELL_INTENT_ARG_FIELD]: "Run unit tests for the parser",
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
const { container } = render(<ToolCallItem toolCall={tc} />);
|
|
173
|
+
|
|
174
|
+
const header = headerOf(container);
|
|
175
|
+
expect(header.textContent).toContain("Run unit tests for the parser");
|
|
176
|
+
expect(header.textContent).toContain("npx vitest run src/parser");
|
|
177
|
+
expect(header.textContent).not.toContain("Shell");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("renders an intent-less shell row exactly as before: bare label, no subtitle", () => {
|
|
181
|
+
const tc = makeToolCall({
|
|
182
|
+
name: "Shell",
|
|
183
|
+
args: { command: "ls -la" },
|
|
184
|
+
});
|
|
185
|
+
const { container } = render(<ToolCallItem toolCall={tc} />);
|
|
186
|
+
|
|
187
|
+
const header = headerOf(container);
|
|
188
|
+
expect(header.textContent).toContain("Shell");
|
|
189
|
+
// The command lives in the terminal body only — never in the header.
|
|
190
|
+
expect(header.textContent).not.toContain("ls -la");
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("ApprovalCardHeader shell title", () => {
|
|
195
|
+
it("titles a gated shell call with the intent phrase", () => {
|
|
196
|
+
const pending = create(PendingApprovalSchema, {
|
|
197
|
+
toolCallId: "tc-1",
|
|
198
|
+
toolName: "execute",
|
|
199
|
+
toolKind: ToolKind.SHELL,
|
|
200
|
+
argsPreview: JSON.stringify({
|
|
201
|
+
command: "rm -rf build",
|
|
202
|
+
[SHELL_INTENT_ARG_FIELD]: "Clean the build directory",
|
|
203
|
+
}),
|
|
204
|
+
});
|
|
205
|
+
render(<ApprovalCardHeader pendingApproval={pending} />);
|
|
206
|
+
|
|
207
|
+
expect(screen.getByText("Clean the build directory")).toBeDefined();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("keeps the bare category label when no intent is present", () => {
|
|
211
|
+
const pending = create(PendingApprovalSchema, {
|
|
212
|
+
toolCallId: "tc-2",
|
|
213
|
+
toolName: "execute",
|
|
214
|
+
toolKind: ToolKind.SHELL,
|
|
215
|
+
argsPreview: JSON.stringify({ command: "rm -rf build" }),
|
|
216
|
+
});
|
|
217
|
+
render(<ApprovalCardHeader pendingApproval={pending} />);
|
|
218
|
+
|
|
219
|
+
expect(screen.getByText("Shell")).toBeDefined();
|
|
220
|
+
});
|
|
221
|
+
});
|
package/src/execution/index.ts
CHANGED
|
@@ -340,6 +340,9 @@ export {
|
|
|
340
340
|
toolKindToCategoryInfo,
|
|
341
341
|
extractPrimaryArg,
|
|
342
342
|
extractPrimaryArgFromPreview,
|
|
343
|
+
extractShellIntent,
|
|
344
|
+
extractShellIntentFromPreview,
|
|
345
|
+
SHELL_INTENT_ARG_FIELD,
|
|
343
346
|
humanizeToolName,
|
|
344
347
|
defaultDisclosureForCategory,
|
|
345
348
|
isRunGroupableCategory,
|
|
@@ -192,6 +192,14 @@ export interface ToolCategoryInfo {
|
|
|
192
192
|
readonly primaryArgField: string;
|
|
193
193
|
/** Fallback argument keys tried when the primary key is absent. */
|
|
194
194
|
readonly fallbackArgFields: readonly string[];
|
|
195
|
+
/**
|
|
196
|
+
* JSON argument key carrying a model-authored intent phrase for this kind
|
|
197
|
+
* (stigmer#276), or `undefined` for kinds without one. Shell is the only
|
|
198
|
+
* carrier today: both harnesses converge on `description` — the Cursor
|
|
199
|
+
* harness's built-in Shell tool ships it natively, and the native harness's
|
|
200
|
+
* tool-intent middleware adds it to the `execute` schema at bind time.
|
|
201
|
+
*/
|
|
202
|
+
readonly intentArgField?: string;
|
|
195
203
|
}
|
|
196
204
|
|
|
197
205
|
interface KindDisplayEntry {
|
|
@@ -199,6 +207,7 @@ interface KindDisplayEntry {
|
|
|
199
207
|
readonly label: string;
|
|
200
208
|
readonly primaryField: string;
|
|
201
209
|
readonly fallbackFields?: readonly string[];
|
|
210
|
+
readonly intentField?: string;
|
|
202
211
|
}
|
|
203
212
|
|
|
204
213
|
// Presentation metadata per ToolKind. Classification (name -> kind) is owned by
|
|
@@ -209,7 +218,7 @@ const KIND_DISPLAY: Partial<Record<ToolKind, KindDisplayEntry>> = {
|
|
|
209
218
|
[ToolKind.FILE_WRITE]: { category: "write", label: "Write", primaryField: "path", fallbackFields: ["file_path", "file", "filename"] },
|
|
210
219
|
[ToolKind.FILE_EDIT]: { category: "edit", label: "Edit", primaryField: "path", fallbackFields: ["file_path", "file", "filename"] },
|
|
211
220
|
[ToolKind.FILE_DELETE]: { category: "delete", label: "Delete", primaryField: "path", fallbackFields: ["file_path", "file", "filename"] },
|
|
212
|
-
[ToolKind.SHELL]: { category: "shell", label: "Shell", primaryField: "command" },
|
|
221
|
+
[ToolKind.SHELL]: { category: "shell", label: "Shell", primaryField: "command", intentField: "description" },
|
|
213
222
|
[ToolKind.SEARCH]: { category: "search", label: "Search", primaryField: "pattern", fallbackFields: ["query", "q"] },
|
|
214
223
|
[ToolKind.LIST]: { category: "list", label: "List", primaryField: "path" },
|
|
215
224
|
[ToolKind.FETCH]: { category: "fetch", label: "Fetch", primaryField: "url", fallbackFields: ["uri"] },
|
|
@@ -264,6 +273,7 @@ export function toolKindToCategoryInfo(
|
|
|
264
273
|
label: entry.label,
|
|
265
274
|
primaryArgField: entry.primaryField,
|
|
266
275
|
fallbackArgFields: entry.fallbackFields ?? [],
|
|
276
|
+
...(entry.intentField ? { intentArgField: entry.intentField } : {}),
|
|
267
277
|
};
|
|
268
278
|
}
|
|
269
279
|
|
|
@@ -440,6 +450,118 @@ export function extractPrimaryArgFromPreview(
|
|
|
440
450
|
}
|
|
441
451
|
}
|
|
442
452
|
|
|
453
|
+
/**
|
|
454
|
+
* Extracts the model-authored intent phrase from a tool call's args
|
|
455
|
+
* (stigmer#276), or `null` when the kind carries none / the model omitted it.
|
|
456
|
+
*
|
|
457
|
+
* The intent is presentation metadata authored by the model *for the user* —
|
|
458
|
+
* "Run unit tests for the parser" — rendered as the row title in place of the
|
|
459
|
+
* bare category label. Extraction is kind-scoped via
|
|
460
|
+
* {@link ToolCategoryInfo.intentArgField} (shell-only today) so a same-named
|
|
461
|
+
* REAL argument on another kind (e.g. the sub-agent task tool's
|
|
462
|
+
* `description`, which is its subject) is never misread as an intent.
|
|
463
|
+
*/
|
|
464
|
+
export function extractIntent(toolCall: ToolCall): string | null {
|
|
465
|
+
const info = resolveToolCategoryFromCall(toolCall);
|
|
466
|
+
if (!info.intentArgField || !toolCall.args) return null;
|
|
467
|
+
const value = toolCall.args[info.intentArgField];
|
|
468
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* {@link extractIntent} for surfaces that carry a JSON `argsPreview` string
|
|
473
|
+
* instead of a full {@link ToolCall} — notably `PendingApproval`. Same
|
|
474
|
+
* kind-scoping; returns `null` on parse failure or when absent.
|
|
475
|
+
*/
|
|
476
|
+
export function extractIntentFromPreview(
|
|
477
|
+
toolName: string,
|
|
478
|
+
argsPreview: string,
|
|
479
|
+
mcpServerSlug?: string,
|
|
480
|
+
toolKind?: ToolKind,
|
|
481
|
+
): string | null {
|
|
482
|
+
if (!argsPreview) return null;
|
|
483
|
+
|
|
484
|
+
const info =
|
|
485
|
+
toolKind !== undefined
|
|
486
|
+
? resolveToolCategoryFromKind(toolKind, toolName, mcpServerSlug)
|
|
487
|
+
: resolveToolCategory(toolName, mcpServerSlug);
|
|
488
|
+
if (!info.intentArgField) return null;
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
const parsed = JSON.parse(argsPreview);
|
|
492
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
493
|
+
const value = (parsed as Record<string, unknown>)[info.intentArgField];
|
|
494
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
495
|
+
} catch {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* The tool-call argument that carries a model-authored intent phrase for
|
|
502
|
+
* SHELL tools (stigmer#276) — a short human description of what the command
|
|
503
|
+
* does and why, rendered as the row title with the command as secondary text.
|
|
504
|
+
*
|
|
505
|
+
* Two writers populate it, converged on one wire key by design: the native
|
|
506
|
+
* harness's tool-intent middleware (`backend/services/runner/src/middleware/
|
|
507
|
+
* tool-intent.ts`, `INTENT_ARG`) extends the shell tool's bind-time schema
|
|
508
|
+
* with it, and the Cursor harness's built-in Shell tool carries it natively.
|
|
509
|
+
* The shared, machine-checked contract is
|
|
510
|
+
* `test/fixtures/tool-view/intent-title.json` — keep all three in lockstep.
|
|
511
|
+
*/
|
|
512
|
+
export const SHELL_INTENT_ARG_FIELD = "description";
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* The intent-title extraction core: SHELL-kind-scoped on purpose, because
|
|
516
|
+
* `description` means other things on other tools (a task tool's description
|
|
517
|
+
* is the sub-agent subject, never a row title). Blank and non-string values
|
|
518
|
+
* degrade to null — the caller falls back to the category label.
|
|
519
|
+
*/
|
|
520
|
+
function extractIntentFromArgs(kind: ToolKind, args: JsonObject | undefined): string | null {
|
|
521
|
+
if (kind !== ToolKind.SHELL || !args) return null;
|
|
522
|
+
const value = args[SHELL_INTENT_ARG_FIELD];
|
|
523
|
+
if (typeof value !== "string") return null;
|
|
524
|
+
const trimmed = value.trim();
|
|
525
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Extracts the model-authored intent phrase from a SHELL tool call, or null
|
|
530
|
+
* when absent (legacy executions, models that skipped the optional arg, and
|
|
531
|
+
* every non-shell kind). See {@link SHELL_INTENT_ARG_FIELD}.
|
|
532
|
+
*/
|
|
533
|
+
export function extractShellIntent(toolCall: ToolCall): string | null {
|
|
534
|
+
return extractIntentFromArgs(resolveToolKind(toolCall), toolCall.args);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Preview-string twin of {@link extractShellIntent} for surfaces that carry a
|
|
539
|
+
* `PendingApproval` (JSON `argsPreview`) instead of a full {@link ToolCall} —
|
|
540
|
+
* mirrors {@link extractPrimaryArgFromPreview}'s kind resolution.
|
|
541
|
+
*/
|
|
542
|
+
export function extractShellIntentFromPreview(
|
|
543
|
+
toolName: string,
|
|
544
|
+
argsPreview: string,
|
|
545
|
+
mcpServerSlug?: string,
|
|
546
|
+
toolKind?: ToolKind,
|
|
547
|
+
): string | null {
|
|
548
|
+
if (!argsPreview) return null;
|
|
549
|
+
|
|
550
|
+
const kind =
|
|
551
|
+
toolKind !== undefined && toolKind !== ToolKind.UNSPECIFIED
|
|
552
|
+
? toolKind
|
|
553
|
+
: resolveToolKindByName(toolName, mcpServerSlug);
|
|
554
|
+
if (kind !== ToolKind.SHELL) return null;
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
const parsed = JSON.parse(argsPreview);
|
|
558
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
559
|
+
return extractIntentFromArgs(kind, parsed as JsonObject);
|
|
560
|
+
} catch {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
443
565
|
const WRITE_CONTENT_FIELDS = [
|
|
444
566
|
"contents",
|
|
445
567
|
"content",
|
|
@@ -19,6 +19,7 @@ import type { ToolResultView } from "@stigmer/sdk";
|
|
|
19
19
|
import {
|
|
20
20
|
resolveToolCategoryFromCall,
|
|
21
21
|
extractPrimaryArg,
|
|
22
|
+
extractShellIntent,
|
|
22
23
|
defaultChromeForCategory,
|
|
23
24
|
defaultDisclosureForCategory,
|
|
24
25
|
isRunGroupableCategory,
|
|
@@ -124,6 +125,21 @@ export interface ToolPresentation {
|
|
|
124
125
|
readonly category: ToolCategory;
|
|
125
126
|
/** Display label, e.g. "Edit", "Shell". */
|
|
126
127
|
readonly label: string;
|
|
128
|
+
/**
|
|
129
|
+
* The model-authored intent phrase for a SHELL call (stigmer#276), or null
|
|
130
|
+
* when absent (legacy executions, skipped optional arg, non-shell kinds).
|
|
131
|
+
* Raw extraction — precedence against overrides is resolved in
|
|
132
|
+
* {@link ToolPresentation.title}.
|
|
133
|
+
*/
|
|
134
|
+
readonly intent: string | null;
|
|
135
|
+
/**
|
|
136
|
+
* The row title: a registered presenter's `label` override wins (a host's
|
|
137
|
+
* explicit wording is never displaced by model output), then the
|
|
138
|
+
* model-authored {@link intent}, then the category {@link label}. Renderers
|
|
139
|
+
* that title a row use this; {@link label} stays the short kind label for
|
|
140
|
+
* chips and badges.
|
|
141
|
+
*/
|
|
142
|
+
readonly title: string;
|
|
127
143
|
/** Primary argument (path, command, pattern...), or null. */
|
|
128
144
|
readonly primaryArg: string | null;
|
|
129
145
|
/** Normalized result view for rendering with {@link ResultView}. */
|
|
@@ -166,7 +182,10 @@ export function useToolPresentation(toolCall: ToolCall): ToolPresentation {
|
|
|
166
182
|
const result = normalizeToolResult(toolCall);
|
|
167
183
|
const override = registry.get(kind);
|
|
168
184
|
|
|
169
|
-
const
|
|
185
|
+
const overrideLabel = override?.label?.(toolCall);
|
|
186
|
+
const label = overrideLabel ?? categoryInfo.label;
|
|
187
|
+
const intent = extractShellIntent(toolCall);
|
|
188
|
+
const title = overrideLabel ?? intent ?? categoryInfo.label;
|
|
170
189
|
const resultSummary =
|
|
171
190
|
override?.summary?.(toolCall, result) ?? summarizeResultView(result);
|
|
172
191
|
const disclosure =
|
|
@@ -183,6 +202,8 @@ export function useToolPresentation(toolCall: ToolCall): ToolPresentation {
|
|
|
183
202
|
kind,
|
|
184
203
|
category: categoryInfo.category,
|
|
185
204
|
label,
|
|
205
|
+
intent,
|
|
206
|
+
title,
|
|
186
207
|
primaryArg: extractPrimaryArg(toolCall),
|
|
187
208
|
result,
|
|
188
209
|
resultSummary,
|
package/src/index.ts
CHANGED