1+ import { isAbsolute , resolve } from "path"
12import type { ToolCall , ToolCallContent , ToolCallLocation , ToolCallUpdate , ToolKind } from "@agentclientprotocol/sdk"
23
34export type ToolInput = Record < string , unknown >
@@ -69,10 +70,16 @@ export function toToolKind(toolName: string): ToolKind {
6970 }
7071}
7172
72- export function toLocations ( toolName : string , input : ToolInput ) : ToolCallLocation [ ] {
73+ export function toLocations ( toolName : string , input : ToolInput , cwd ?: string ) : ToolCallLocation [ ] {
7374 const tool = toolName . toLocaleLowerCase ( )
7475
7576 switch ( tool ) {
77+ case "bash" :
78+ case "shell" : {
79+ const workdir = shellWorkdir ( input , cwd )
80+ return workdir ? [ { path : workdir } ] : [ ]
81+ }
82+
7683 case "read" :
7784 case "edit" :
7885 case "write" :
@@ -88,10 +95,6 @@ export function toLocations(toolName: string, input: ToolInput): ToolCallLocatio
8895 case "context7_get_library_docs" :
8996 return locationFrom ( input . path )
9097
91- case "bash" :
92- case "shell" :
93- return [ ]
94-
9598 default :
9699 return [ ]
97100 }
@@ -122,14 +125,15 @@ export function pendingToolCall(input: {
122125 readonly toolCallId : string
123126 readonly toolName : string
124127 readonly state : { readonly input : ToolInput ; readonly title ?: string }
128+ readonly cwd ?: string
125129} ) : ToolCall {
126130 return {
127131 toolCallId : input . toolCallId ,
128- title : input . state . title || input . toolName ,
132+ title : toolTitle ( input . toolName , input . state . input , input . state . title ) ,
129133 kind : toToolKind ( input . toolName ) ,
130134 status : "pending" ,
131- locations : toLocations ( input . toolName , input . state . input ) ,
132- rawInput : input . state . input ,
135+ locations : toLocations ( input . toolName , input . state . input , input . cwd ) ,
136+ rawInput : rawInput ( input . toolName , input . state . input , input . cwd ) ,
133137 }
134138}
135139
@@ -138,6 +142,7 @@ export function runningToolUpdate(input: {
138142 readonly toolName : string
139143 readonly state : RunningToolState
140144 readonly output ?: string
145+ readonly cwd ?: string
141146} ) : ToolCallUpdate {
142147 const content = input . output
143148 ? [
@@ -155,9 +160,9 @@ export function runningToolUpdate(input: {
155160 toolCallId : input . toolCallId ,
156161 status : "in_progress" ,
157162 kind : toToolKind ( input . toolName ) ,
158- title : input . state . title ?? input . toolName ,
159- locations : toLocations ( input . toolName , input . state . input ) ,
160- rawInput : input . state . input ,
163+ title : toolTitle ( input . toolName , input . state . input , input . state . title ) ,
164+ locations : toLocations ( input . toolName , input . state . input , input . cwd ) ,
165+ rawInput : rawInput ( input . toolName , input . state . input , input . cwd ) ,
161166 ...( content ? { content } : { } ) ,
162167 }
163168}
@@ -166,29 +171,32 @@ export function duplicateRunningToolUpdate(input: {
166171 readonly toolCallId : string
167172 readonly toolName : string
168173 readonly state : RunningToolState
174+ readonly cwd ?: string
169175} ) : ToolCallUpdate {
170176 return {
171177 toolCallId : input . toolCallId ,
172178 status : "in_progress" ,
173179 kind : toToolKind ( input . toolName ) ,
174- title : input . state . title ?? input . toolName ,
175- locations : toLocations ( input . toolName , input . state . input ) ,
176- rawInput : input . state . input ,
180+ title : toolTitle ( input . toolName , input . state . input , input . state . title ) ,
181+ locations : toLocations ( input . toolName , input . state . input , input . cwd ) ,
182+ rawInput : rawInput ( input . toolName , input . state . input , input . cwd ) ,
177183 }
178184}
179185
180186export function completedToolUpdate ( input : {
181187 readonly toolCallId : string
182188 readonly toolName : string
183- readonly state : CompletedToolState & { readonly title : string }
189+ readonly state : CompletedToolState & { readonly title ?: string }
190+ readonly cwd ?: string
184191} ) : ToolCallUpdate {
185192 return {
186193 toolCallId : input . toolCallId ,
187194 status : "completed" ,
188195 kind : toToolKind ( input . toolName ) ,
189- title : input . state . title ,
196+ title : toolTitle ( input . toolName , input . state . input , input . state . title ) ,
197+ locations : toLocations ( input . toolName , input . state . input , input . cwd ) ,
190198 content : completedToolContent ( input . toolName , input . state ) ,
191- rawInput : input . state . input ,
199+ rawInput : rawInput ( input . toolName , input . state . input , input . cwd ) ,
192200 rawOutput : completedToolRawOutput ( input . state ) ,
193201 }
194202}
@@ -197,13 +205,15 @@ export function errorToolUpdate(input: {
197205 readonly toolCallId : string
198206 readonly toolName : string
199207 readonly state : ErrorToolState
208+ readonly cwd ?: string
200209} ) : ToolCallUpdate {
201210 return {
202211 toolCallId : input . toolCallId ,
203212 status : "failed" ,
204213 kind : toToolKind ( input . toolName ) ,
205- title : input . toolName ,
206- rawInput : input . state . input ,
214+ title : toolTitle ( input . toolName , input . state . input , undefined ) ,
215+ locations : toLocations ( input . toolName , input . state . input , input . cwd ) ,
216+ rawInput : rawInput ( input . toolName , input . state . input , input . cwd ) ,
207217 content : [
208218 {
209219 type : "content" ,
@@ -253,6 +263,42 @@ export function shellOutputSnapshot(state: { readonly metadata?: unknown }) {
253263 return stringValue ( ( state . metadata as Record < string , unknown > ) . output )
254264}
255265
266+ // For shell tools, surface the actual command as the title so it stays visible
267+ // before output lands; non-shell tools keep their model-provided title.
268+ function toolTitle ( toolName : string , input : ToolInput , fallback : string | undefined ) {
269+ if ( isShell ( toolName ) ) return shellCommand ( input ) ?? stringValue ( input . description ) ?? fallback ?? toolName
270+ return fallback || toolName
271+ }
272+
273+ // Enrich shell rawInput with the resolved working directory so clients can show
274+ // where the command runs, unless the model already specified one.
275+ function rawInput ( toolName : string , input : ToolInput , cwd ?: string ) : ToolInput {
276+ if ( ! isShell ( toolName ) ) return input
277+ if ( input . cwd || input . workdir ) return input
278+ const workdir = shellWorkdir ( input , cwd )
279+ return workdir ? { ...input , cwd : workdir } : input
280+ }
281+
282+ function shellWorkdir ( input : ToolInput , cwd ?: string ) {
283+ const explicit = stringValue ( input . workdir ) ?? stringValue ( input . cwd )
284+ return resolvePath ( explicit , cwd ) ?? cwd
285+ }
286+
287+ function resolvePath ( value : string | undefined , cwd ?: string ) {
288+ if ( ! value ) return undefined
289+ if ( isAbsolute ( value ) ) return value
290+ return resolve ( cwd ?? process . cwd ( ) , value )
291+ }
292+
293+ function shellCommand ( input : ToolInput ) {
294+ return stringValue ( input . command ) ?? stringValue ( input . cmd )
295+ }
296+
297+ function isShell ( toolName : string ) {
298+ const tool = toolName . toLocaleLowerCase ( )
299+ return tool === "bash" || tool === "shell"
300+ }
301+
256302export const mapToolKind = toToolKind
257303export const extractLocations = toLocations
258304export const buildCompletedToolContent = completedToolContent
0 commit comments